Skip to main content

trustformers_debug/
auto_debugger.rs

1//! Automated debugging system for common issues and optimization suggestions
2// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
3// are retained for the data model, serialization completeness, and future consumers that
4// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
5#![allow(dead_code)]
6
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::time::Duration;
11
12use crate::{
13    AnomalyDetectorReport, DashboardMetrics, DebugConfig, GradientDebugReport, ProfilerReport,
14};
15
16/// Automated debugging system
17#[derive(Debug)]
18pub struct AutoDebugger {
19    config: DebugConfig,
20    issue_detectors: Vec<Box<dyn IssueDetector>>,
21    fix_suggestions: HashMap<IssueType, Vec<FixSuggestion>>,
22    optimization_history: Vec<OptimizationAttempt>,
23    knowledge_base: KnowledgeBase,
24}
25
26/// Common training and model issues
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum IssueType {
29    // Training Issues
30    VanishingGradients,
31    ExplodingGradients,
32    LearningRateProblems,
33    OverfittingDetected,
34    UnderfittingDetected,
35    TrainingStalled,
36    LossNotDecreasing,
37    UnstableTraining,
38    MemoryIssues,
39
40    // Model Architecture Issues
41    ModelTooLarge,
42    ModelTooSmall,
43    InappropriateArchitecture,
44    LayerMismatch,
45    ActivationProblems,
46
47    // Data Issues
48    DataImbalance,
49    DataLeakage,
50    InsufficientData,
51    DataQualityIssues,
52    BatchSizeProblems,
53
54    // Performance Issues
55    SlowTraining,
56    LowGpuUtilization,
57    MemoryBottleneck,
58    IoBottleneck,
59    ComputeBottleneck,
60
61    // Hyperparameter Issues
62    LearningRateTooHigh,
63    LearningRateTooLow,
64    BatchSizeTooLarge,
65    BatchSizeTooSmall,
66    RegularizationIssues,
67}
68
69/// Issue detection result
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct DetectedIssue {
72    pub issue_type: IssueType,
73    pub severity: IssueSeverity,
74    pub confidence: f64,
75    pub description: String,
76    pub evidence: Vec<Evidence>,
77    pub metrics: HashMap<String, f64>,
78    pub detected_at: chrono::DateTime<chrono::Utc>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub enum IssueSeverity {
83    Critical,
84    High,
85    Medium,
86    Low,
87    Info,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Evidence {
92    pub metric_name: String,
93    pub observed_value: f64,
94    pub expected_range: (f64, f64),
95    pub explanation: String,
96}
97
98/// Fix suggestion with implementation guidance
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct FixSuggestion {
101    pub fix_id: String,
102    pub fix_type: FixType,
103    pub title: String,
104    pub description: String,
105    pub implementation_steps: Vec<String>,
106    pub expected_impact: ExpectedImpact,
107    pub priority: FixPriority,
108    pub estimated_effort: EstimatedEffort,
109    pub prerequisites: Vec<String>,
110    pub code_examples: Vec<CodeExample>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub enum FixType {
115    HyperparameterAdjustment,
116    ArchitectureChange,
117    TrainingProcedure,
118    DataProcessing,
119    OptimizationTechnique,
120    EnvironmentConfig,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ExpectedImpact {
125    pub performance_improvement: f64,
126    pub training_speed_improvement: f64,
127    pub stability_improvement: f64,
128    pub memory_usage_change: f64,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub enum FixPriority {
133    Critical,
134    High,
135    Medium,
136    Low,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub enum EstimatedEffort {
141    Trivial, // < 5 minutes
142    Easy,    // 5-30 minutes
143    Medium,  // 30 minutes - 2 hours
144    Hard,    // 2-8 hours
145    Complex, // > 8 hours
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct CodeExample {
150    pub language: String,
151    pub code: String,
152    pub explanation: String,
153}
154
155/// Optimization attempt tracking
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct OptimizationAttempt {
158    pub attempt_id: String,
159    pub issue_addressed: IssueType,
160    pub fix_applied: String,
161    pub before_metrics: HashMap<String, f64>,
162    pub after_metrics: Option<HashMap<String, f64>>,
163    pub success: Option<bool>,
164    pub notes: String,
165    pub timestamp: chrono::DateTime<chrono::Utc>,
166}
167
168/// Knowledge base for common patterns and solutions
169#[derive(Debug)]
170pub struct KnowledgeBase {
171    issue_patterns: HashMap<IssueType, IssuePattern>,
172    hyperparameter_recommendations: HashMap<String, HyperparameterAdvice>,
173    architecture_patterns: Vec<ArchitecturePattern>,
174    best_practices: HashMap<String, Vec<String>>,
175}
176
177#[derive(Debug, Clone)]
178pub struct IssuePattern {
179    pub symptoms: Vec<String>,
180    pub common_causes: Vec<String>,
181    pub diagnostic_metrics: Vec<String>,
182    pub typical_solutions: Vec<String>,
183}
184
185#[derive(Debug, Clone)]
186pub struct HyperparameterAdvice {
187    pub parameter_name: String,
188    pub recommended_range: (f64, f64),
189    pub tuning_strategy: String,
190    pub dependencies: Vec<String>,
191    pub common_mistakes: Vec<String>,
192}
193
194#[derive(Debug, Clone)]
195pub struct ArchitecturePattern {
196    pub pattern_name: String,
197    pub use_cases: Vec<String>,
198    pub typical_layers: Vec<String>,
199    pub hyperparameter_suggestions: HashMap<String, f64>,
200    pub performance_characteristics: String,
201}
202
203/// Issue detector trait for modular detection
204pub trait IssueDetector: std::fmt::Debug {
205    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>>;
206    fn get_detector_name(&self) -> &str;
207    fn get_supported_issues(&self) -> Vec<IssueType>;
208}
209
210/// Context for issue detection
211#[derive(Debug)]
212pub struct DebugContext<'a> {
213    pub profiler_report: Option<&'a ProfilerReport>,
214    pub gradient_report: Option<&'a GradientDebugReport>,
215    pub anomaly_report: Option<&'a AnomalyDetectorReport>,
216    pub recent_metrics: &'a [DashboardMetrics],
217    pub training_duration: Duration,
218    pub model_info: Option<&'a ModelInfo>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct ModelInfo {
223    pub model_type: String,
224    pub parameter_count: usize,
225    pub layer_count: usize,
226    pub architecture_details: HashMap<String, String>,
227}
228
229impl AutoDebugger {
230    /// Create new auto-debugger with default detectors
231    pub fn new(config: &DebugConfig) -> Self {
232        let mut auto_debugger = Self {
233            config: config.clone(),
234            issue_detectors: Vec::new(),
235            fix_suggestions: HashMap::new(),
236            optimization_history: Vec::new(),
237            knowledge_base: KnowledgeBase::new(),
238        };
239
240        // Register default detectors
241        auto_debugger.register_default_detectors();
242        auto_debugger.initialize_fix_suggestions();
243
244        auto_debugger
245    }
246
247    /// Register all default issue detectors
248    fn register_default_detectors(&mut self) {
249        self.issue_detectors.push(Box::new(GradientIssueDetector::new()));
250        self.issue_detectors.push(Box::new(TrainingIssueDetector::new()));
251        self.issue_detectors.push(Box::new(PerformanceIssueDetector::new()));
252        self.issue_detectors.push(Box::new(HyperparameterIssueDetector::new()));
253        self.issue_detectors.push(Box::new(ArchitectureIssueDetector::new()));
254        self.issue_detectors.push(Box::new(DataIssueDetector::new()));
255    }
256
257    /// Initialize fix suggestions for common issues
258    fn initialize_fix_suggestions(&mut self) {
259        // Vanishing gradients fixes
260        self.fix_suggestions.insert(
261            IssueType::VanishingGradients,
262            vec![
263                FixSuggestion {
264                    fix_id: "vg_001".to_string(),
265                    fix_type: FixType::ArchitectureChange,
266                    title: "Add Residual Connections".to_string(),
267                    description:
268                        "Implement skip connections to help gradients flow through deep networks"
269                            .to_string(),
270                    implementation_steps: vec![
271                        "Add residual blocks to your model architecture".to_string(),
272                        "Ensure input and output dimensions match for residual connections"
273                            .to_string(),
274                        "Consider using batch normalization within residual blocks".to_string(),
275                    ],
276                    expected_impact: ExpectedImpact {
277                        performance_improvement: 0.15,
278                        training_speed_improvement: 0.05,
279                        stability_improvement: 0.25,
280                        memory_usage_change: 0.02,
281                    },
282                    priority: FixPriority::High,
283                    estimated_effort: EstimatedEffort::Medium,
284                    prerequisites: vec!["Model architecture access".to_string()],
285                    code_examples: vec![CodeExample {
286                        language: "python".to_string(),
287                        code: r#"
288class ResidualBlock(nn.Module):
289    def __init__(self, channels):
290        super().__init__()
291        self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
292        self.bn1 = nn.BatchNorm2d(channels)
293        self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
294        self.bn2 = nn.BatchNorm2d(channels)
295
296    def forward(self, x):
297        residual = x
298        out = F.relu(self.bn1(self.conv1(x)))
299        out = self.bn2(self.conv2(out))
300        out += residual  # Skip connection
301        return F.relu(out)
302"#
303                        .to_string(),
304                        explanation: "Basic residual block implementation with skip connection"
305                            .to_string(),
306                    }],
307                },
308                FixSuggestion {
309                    fix_id: "vg_002".to_string(),
310                    fix_type: FixType::HyperparameterAdjustment,
311                    title: "Adjust Learning Rate".to_string(),
312                    description:
313                        "Increase learning rate to help gradients propagate more effectively"
314                            .to_string(),
315                    implementation_steps: vec![
316                        "Increase learning rate by 2-5x".to_string(),
317                        "Monitor training stability".to_string(),
318                        "Consider learning rate scheduling".to_string(),
319                    ],
320                    expected_impact: ExpectedImpact {
321                        performance_improvement: 0.08,
322                        training_speed_improvement: 0.10,
323                        stability_improvement: -0.05,
324                        memory_usage_change: 0.0,
325                    },
326                    priority: FixPriority::Medium,
327                    estimated_effort: EstimatedEffort::Trivial,
328                    prerequisites: vec![],
329                    code_examples: vec![CodeExample {
330                        language: "python".to_string(),
331                        code: "optimizer = torch.optim.Adam(model.parameters(), lr=0.01)"
332                            .to_string(),
333                        explanation: "Increase learning rate to help overcome vanishing gradients"
334                            .to_string(),
335                    }],
336                },
337            ],
338        );
339
340        // Exploding gradients fixes
341        self.fix_suggestions.insert(
342            IssueType::ExplodingGradients,
343            vec![FixSuggestion {
344                fix_id: "eg_001".to_string(),
345                fix_type: FixType::TrainingProcedure,
346                title: "Apply Gradient Clipping".to_string(),
347                description: "Clip gradients to prevent explosion during backpropagation"
348                    .to_string(),
349                implementation_steps: vec![
350                    "Add gradient clipping to your training loop".to_string(),
351                    "Start with clip value of 1.0 and adjust based on results".to_string(),
352                    "Monitor gradient norms to ensure clipping is effective".to_string(),
353                ],
354                expected_impact: ExpectedImpact {
355                    performance_improvement: 0.10,
356                    training_speed_improvement: 0.0,
357                    stability_improvement: 0.30,
358                    memory_usage_change: 0.0,
359                },
360                priority: FixPriority::Critical,
361                estimated_effort: EstimatedEffort::Easy,
362                prerequisites: vec![],
363                code_examples: vec![CodeExample {
364                    language: "python".to_string(),
365                    code: "torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)"
366                        .to_string(),
367                    explanation: "Clip gradients before optimizer step".to_string(),
368                }],
369            }],
370        );
371
372        // Learning rate issues
373        self.fix_suggestions.insert(
374            IssueType::LearningRateTooHigh,
375            vec![FixSuggestion {
376                fix_id: "lr_high_001".to_string(),
377                fix_type: FixType::HyperparameterAdjustment,
378                title: "Reduce Learning Rate".to_string(),
379                description: "Lower the learning rate to improve training stability".to_string(),
380                implementation_steps: vec![
381                    "Reduce learning rate by 2-10x".to_string(),
382                    "Consider learning rate scheduling".to_string(),
383                    "Monitor loss convergence".to_string(),
384                ],
385                expected_impact: ExpectedImpact {
386                    performance_improvement: 0.12,
387                    training_speed_improvement: -0.05,
388                    stability_improvement: 0.25,
389                    memory_usage_change: 0.0,
390                },
391                priority: FixPriority::High,
392                estimated_effort: EstimatedEffort::Trivial,
393                prerequisites: vec![],
394                code_examples: vec![CodeExample {
395                    language: "python".to_string(),
396                    code: "optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)".to_string(),
397                    explanation: "Reduce learning rate for more stable training".to_string(),
398                }],
399            }],
400        );
401
402        // Performance issues
403        self.fix_suggestions.insert(
404            IssueType::LowGpuUtilization,
405            vec![FixSuggestion {
406                fix_id: "gpu_001".to_string(),
407                fix_type: FixType::HyperparameterAdjustment,
408                title: "Increase Batch Size".to_string(),
409                description: "Increase batch size to better utilize GPU compute capacity"
410                    .to_string(),
411                implementation_steps: vec![
412                    "Double the current batch size".to_string(),
413                    "Monitor memory usage to avoid OOM".to_string(),
414                    "Adjust learning rate proportionally".to_string(),
415                ],
416                expected_impact: ExpectedImpact {
417                    performance_improvement: 0.05,
418                    training_speed_improvement: 0.30,
419                    stability_improvement: 0.0,
420                    memory_usage_change: 0.20,
421                },
422                priority: FixPriority::Medium,
423                estimated_effort: EstimatedEffort::Easy,
424                prerequisites: vec!["Available GPU memory".to_string()],
425                code_examples: vec![CodeExample {
426                    language: "python".to_string(),
427                    code: "train_loader = DataLoader(dataset, batch_size=64, shuffle=True)"
428                        .to_string(),
429                    explanation: "Increase batch size to improve GPU utilization".to_string(),
430                }],
431            }],
432        );
433    }
434
435    /// Analyze debug context and detect issues
436    pub fn analyze_issues(&self, context: &DebugContext) -> Result<AutoDebugReport> {
437        let mut all_issues = Vec::new();
438
439        // Run all issue detectors
440        for detector in &self.issue_detectors {
441            match detector.detect_issues(context) {
442                Ok(mut issues) => all_issues.append(&mut issues),
443                Err(e) => {
444                    tracing::warn!(
445                        "Issue detector '{}' failed: {}",
446                        detector.get_detector_name(),
447                        e
448                    );
449                },
450            }
451        }
452
453        // Sort issues by severity and confidence
454        all_issues.sort_by(|a, b| {
455            let severity_order = |s: &IssueSeverity| match s {
456                IssueSeverity::Critical => 0,
457                IssueSeverity::High => 1,
458                IssueSeverity::Medium => 2,
459                IssueSeverity::Low => 3,
460                IssueSeverity::Info => 4,
461            };
462
463            let severity_cmp = severity_order(&a.severity).cmp(&severity_order(&b.severity));
464            if severity_cmp == std::cmp::Ordering::Equal {
465                b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)
466            } else {
467                severity_cmp
468            }
469        });
470
471        // Generate fix recommendations
472        let fix_recommendations = self.generate_fix_recommendations(&all_issues);
473
474        // Generate hyperparameter recommendations
475        let hyperparameter_recommendations = self.generate_hyperparameter_recommendations(context);
476
477        // Generate architecture suggestions
478        let architecture_suggestions = self.generate_architecture_suggestions(context);
479
480        // Generate training recipe optimization
481        let training_recipe = self.generate_training_recipe_optimization(context);
482
483        Ok(AutoDebugReport {
484            detected_issues: all_issues,
485            fix_recommendations: fix_recommendations.clone(),
486            hyperparameter_recommendations,
487            architecture_suggestions,
488            training_recipe,
489            analysis_summary: self.generate_analysis_summary(&fix_recommendations),
490            confidence_score: self.calculate_overall_confidence(&fix_recommendations),
491        })
492    }
493
494    /// Generate fix recommendations for detected issues
495    fn generate_fix_recommendations(&self, issues: &[DetectedIssue]) -> Vec<FixRecommendation> {
496        let mut recommendations = Vec::new();
497
498        for issue in issues {
499            if let Some(suggestions) = self.fix_suggestions.get(&issue.issue_type) {
500                for suggestion in suggestions {
501                    recommendations.push(FixRecommendation {
502                        issue: issue.clone(),
503                        fix_suggestion: suggestion.clone(),
504                        confidence: issue.confidence * 0.9, // Slightly reduce confidence
505                        urgency: self.calculate_urgency(issue),
506                    });
507                }
508            }
509        }
510
511        // Sort by urgency and confidence
512        recommendations.sort_by(|a, b| {
513            let urgency_cmp =
514                b.urgency.partial_cmp(&a.urgency).unwrap_or(std::cmp::Ordering::Equal);
515            if urgency_cmp == std::cmp::Ordering::Equal {
516                b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)
517            } else {
518                urgency_cmp
519            }
520        });
521
522        recommendations
523    }
524
525    fn calculate_urgency(&self, issue: &DetectedIssue) -> f64 {
526        let severity_multiplier = match issue.severity {
527            IssueSeverity::Critical => 1.0,
528            IssueSeverity::High => 0.8,
529            IssueSeverity::Medium => 0.6,
530            IssueSeverity::Low => 0.4,
531            IssueSeverity::Info => 0.2,
532        };
533
534        issue.confidence * severity_multiplier
535    }
536
537    /// Generate hyperparameter recommendations
538    fn generate_hyperparameter_recommendations(
539        &self,
540        context: &DebugContext,
541    ) -> Vec<HyperparameterRecommendation> {
542        let mut recommendations = Vec::new();
543
544        // Learning rate recommendations
545        if let Some(metrics) = context.recent_metrics.last() {
546            if let Some(loss) = metrics.loss {
547                if loss > 1.0 {
548                    recommendations.push(HyperparameterRecommendation {
549                        parameter: "learning_rate".to_string(),
550                        current_value: None,
551                        recommended_value: 0.001,
552                        reason: "High loss suggests learning rate might be too low".to_string(),
553                        confidence: 0.7,
554                    });
555                }
556            }
557        }
558
559        // No batch-size recommendation is emitted. It used to push a fixed
560        // `recommended_value: 32.0` with `confidence: 0.6` whenever a profiler
561        // report was merely PRESENT, without reading a single field of it --
562        // so every run of every model got the same advice at the same stated
563        // confidence. A real recommendation needs measured GPU utilization and
564        // memory headroom against the current batch size, none of which this
565        // context carries.
566
567        recommendations
568    }
569
570    /// Generate architecture suggestions
571    fn generate_architecture_suggestions(
572        &self,
573        context: &DebugContext,
574    ) -> Vec<ArchitectureSuggestion> {
575        let mut suggestions = Vec::new();
576
577        // Analyze model size vs performance
578        if let Some(model_info) = context.model_info {
579            if model_info.parameter_count > 100_000_000 {
580                suggestions.push(ArchitectureSuggestion {
581                    suggestion_type: "model_compression".to_string(),
582                    title: "Consider Model Compression".to_string(),
583                    description: "Large model may benefit from pruning or distillation".to_string(),
584                    impact_assessment: "Reduce memory usage by 20-50% with minimal accuracy loss"
585                        .to_string(),
586                    implementation_difficulty: "Medium".to_string(),
587                });
588            }
589
590            if model_info.layer_count > 50 {
591                suggestions.push(ArchitectureSuggestion {
592                    suggestion_type: "depth_optimization".to_string(),
593                    title: "Optimize Network Depth".to_string(),
594                    description: "Very deep network may suffer from gradient flow issues"
595                        .to_string(),
596                    impact_assessment: "Improve training stability and convergence speed"
597                        .to_string(),
598                    implementation_difficulty: "High".to_string(),
599                });
600            }
601        }
602
603        suggestions
604    }
605
606    /// Generate training recipe optimization
607    fn generate_training_recipe_optimization(
608        &self,
609        context: &DebugContext,
610    ) -> TrainingRecipeOptimization {
611        let mut optimizations = Vec::new();
612
613        // Analyze training duration and suggest optimizations
614        if context.training_duration > Duration::from_secs(3600) {
615            optimizations
616                .push("Consider learning rate scheduling to speed up convergence".to_string());
617            optimizations.push("Implement early stopping to avoid overtraining".to_string());
618        }
619
620        // Analyze recent metrics for training recipe suggestions
621        if context.recent_metrics.len() > 10 {
622            let recent_losses: Vec<f64> =
623                context.recent_metrics.iter().rev().take(10).filter_map(|m| m.loss).collect();
624
625            if recent_losses.len() >= 5 {
626                let variance = self.calculate_variance(&recent_losses);
627                if variance > 0.1 {
628                    optimizations.push(
629                        "Training loss is unstable - consider reducing learning rate".to_string(),
630                    );
631                }
632            }
633        }
634
635        TrainingRecipeOptimization {
636            recommended_optimizations: optimizations,
637            training_schedule: TrainingSchedule {
638                warmup_steps: 1000,
639                learning_rate_schedule: "cosine_annealing".to_string(),
640                batch_size_schedule: "constant".to_string(),
641                early_stopping: true,
642                checkpoint_frequency: 1000,
643            },
644            data_strategy: DataStrategy {
645                data_augmentation: vec!["horizontal_flip".to_string(), "random_crop".to_string()],
646                sampling_strategy: "balanced".to_string(),
647                preprocessing_optimizations: vec![
648                    "normalization".to_string(),
649                    "standardization".to_string(),
650                ],
651            },
652        }
653    }
654
655    fn calculate_variance(&self, values: &[f64]) -> f64 {
656        if values.len() < 2 {
657            return 0.0;
658        }
659
660        let mean = values.iter().sum::<f64>() / values.len() as f64;
661        let variance =
662            values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
663        variance
664    }
665
666    fn generate_analysis_summary(&self, recommendations: &[FixRecommendation]) -> String {
667        let critical_count = recommendations
668            .iter()
669            .filter(|r| matches!(r.issue.severity, IssueSeverity::Critical))
670            .count();
671
672        let high_count = recommendations
673            .iter()
674            .filter(|r| matches!(r.issue.severity, IssueSeverity::High))
675            .count();
676
677        if critical_count > 0 {
678            format!("Found {} critical issues requiring immediate attention. {} high-priority issues also detected.",
679                   critical_count, high_count)
680        } else if high_count > 0 {
681            format!(
682                "Found {} high-priority issues that should be addressed soon.",
683                high_count
684            )
685        } else if !recommendations.is_empty() {
686            "Found some optimization opportunities to improve training performance.".to_string()
687        } else {
688            "No significant issues detected. Training appears to be proceeding normally."
689                .to_string()
690        }
691    }
692
693    fn calculate_overall_confidence(&self, recommendations: &[FixRecommendation]) -> f64 {
694        if recommendations.is_empty() {
695            return 1.0;
696        }
697
698        let sum_confidence: f64 = recommendations.iter().map(|r| r.confidence).sum();
699        sum_confidence / recommendations.len() as f64
700    }
701
702    /// Record optimization attempt for learning
703    pub fn record_optimization_attempt(&mut self, attempt: OptimizationAttempt) {
704        self.optimization_history.push(attempt);
705
706        // Keep only recent attempts to prevent unbounded growth
707        if self.optimization_history.len() > 1000 {
708            self.optimization_history.drain(0..500);
709        }
710    }
711
712    /// Get optimization history for analysis
713    pub fn get_optimization_history(&self) -> &[OptimizationAttempt] {
714        &self.optimization_history
715    }
716}
717
718// Issue detector implementations
719
720#[derive(Debug)]
721struct GradientIssueDetector;
722
723impl GradientIssueDetector {
724    fn new() -> Self {
725        Self
726    }
727}
728
729impl IssueDetector for GradientIssueDetector {
730    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>> {
731        let mut issues = Vec::new();
732
733        if let Some(gradient_report) = context.gradient_report {
734            // Check for vanishing gradients
735            if gradient_report.has_vanishing_gradients() {
736                issues.push(DetectedIssue {
737                    issue_type: IssueType::VanishingGradients,
738                    severity: IssueSeverity::High,
739                    confidence: 0.9,
740                    description: "Vanishing gradients detected in multiple layers".to_string(),
741                    evidence: vec![Evidence {
742                        metric_name: "gradient_norm".to_string(),
743                        observed_value: 0.001,
744                        expected_range: (0.01, 1.0),
745                        explanation: "Gradient norms are significantly below normal range"
746                            .to_string(),
747                    }],
748                    metrics: HashMap::new(),
749                    detected_at: chrono::Utc::now(),
750                });
751            }
752
753            // Check for exploding gradients
754            if gradient_report.has_exploding_gradients() {
755                issues.push(DetectedIssue {
756                    issue_type: IssueType::ExplodingGradients,
757                    severity: IssueSeverity::Critical,
758                    confidence: 0.95,
759                    description: "Exploding gradients detected - training instability likely"
760                        .to_string(),
761                    evidence: vec![Evidence {
762                        metric_name: "gradient_norm".to_string(),
763                        observed_value: 100.0,
764                        expected_range: (0.01, 10.0),
765                        explanation: "Gradient norms are extremely high".to_string(),
766                    }],
767                    metrics: HashMap::new(),
768                    detected_at: chrono::Utc::now(),
769                });
770            }
771        }
772
773        Ok(issues)
774    }
775
776    fn get_detector_name(&self) -> &str {
777        "GradientIssueDetector"
778    }
779
780    fn get_supported_issues(&self) -> Vec<IssueType> {
781        vec![IssueType::VanishingGradients, IssueType::ExplodingGradients]
782    }
783}
784
785#[derive(Debug)]
786struct TrainingIssueDetector;
787
788impl TrainingIssueDetector {
789    fn new() -> Self {
790        Self
791    }
792}
793
794impl IssueDetector for TrainingIssueDetector {
795    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>> {
796        let mut issues = Vec::new();
797
798        // Analyze recent training metrics
799        if context.recent_metrics.len() >= 10 {
800            let recent_losses: Vec<f64> =
801                context.recent_metrics.iter().rev().take(10).filter_map(|m| m.loss).collect();
802
803            if recent_losses.len() >= 5 {
804                // Check for stalled training
805                let first_half_avg = recent_losses[..recent_losses.len() / 2].iter().sum::<f64>()
806                    / (recent_losses.len() / 2) as f64;
807                let second_half_avg = recent_losses[recent_losses.len() / 2..].iter().sum::<f64>()
808                    / (recent_losses.len() - recent_losses.len() / 2) as f64;
809
810                if (first_half_avg - second_half_avg).abs() / first_half_avg < 0.01 {
811                    issues.push(DetectedIssue {
812                        issue_type: IssueType::TrainingStalled,
813                        severity: IssueSeverity::Medium,
814                        confidence: 0.8,
815                        description: "Training appears to have stalled - loss not decreasing"
816                            .to_string(),
817                        evidence: vec![Evidence {
818                            metric_name: "loss_change".to_string(),
819                            observed_value: (first_half_avg - second_half_avg).abs()
820                                / first_half_avg,
821                            expected_range: (0.05, 1.0),
822                            explanation: "Loss change is below expected threshold".to_string(),
823                        }],
824                        metrics: HashMap::new(),
825                        detected_at: chrono::Utc::now(),
826                    });
827                }
828            }
829        }
830
831        Ok(issues)
832    }
833
834    fn get_detector_name(&self) -> &str {
835        "TrainingIssueDetector"
836    }
837
838    fn get_supported_issues(&self) -> Vec<IssueType> {
839        vec![
840            IssueType::TrainingStalled,
841            IssueType::LossNotDecreasing,
842            IssueType::UnstableTraining,
843        ]
844    }
845}
846
847#[derive(Debug)]
848struct PerformanceIssueDetector;
849
850impl PerformanceIssueDetector {
851    fn new() -> Self {
852        Self
853    }
854}
855
856impl IssueDetector for PerformanceIssueDetector {
857    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>> {
858        let mut issues = Vec::new();
859
860        // Check GPU utilization
861        if let Some(metrics) = context.recent_metrics.last() {
862            if let Some(gpu_util) = metrics.gpu_utilization {
863                if gpu_util < 0.5 {
864                    issues.push(DetectedIssue {
865                        issue_type: IssueType::LowGpuUtilization,
866                        severity: IssueSeverity::Medium,
867                        confidence: 0.8,
868                        description:
869                            "Low GPU utilization detected - compute resources underutilized"
870                                .to_string(),
871                        evidence: vec![Evidence {
872                            metric_name: "gpu_utilization".to_string(),
873                            observed_value: gpu_util,
874                            expected_range: (0.7, 1.0),
875                            explanation: "GPU utilization is below optimal range".to_string(),
876                        }],
877                        metrics: HashMap::new(),
878                        detected_at: chrono::Utc::now(),
879                    });
880                }
881            }
882        }
883
884        Ok(issues)
885    }
886
887    fn get_detector_name(&self) -> &str {
888        "PerformanceIssueDetector"
889    }
890
891    fn get_supported_issues(&self) -> Vec<IssueType> {
892        vec![
893            IssueType::LowGpuUtilization,
894            IssueType::SlowTraining,
895            IssueType::MemoryBottleneck,
896        ]
897    }
898}
899
900#[derive(Debug)]
901struct HyperparameterIssueDetector;
902
903impl HyperparameterIssueDetector {
904    fn new() -> Self {
905        Self
906    }
907}
908
909impl IssueDetector for HyperparameterIssueDetector {
910    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>> {
911        let mut issues = Vec::new();
912
913        if let Some(metrics) = context.recent_metrics.last() {
914            // Check learning rate issues
915            if let Some(lr) = metrics.learning_rate {
916                if lr > 0.1 {
917                    issues.push(DetectedIssue {
918                        issue_type: IssueType::LearningRateTooHigh,
919                        severity: IssueSeverity::High,
920                        confidence: 0.7,
921                        description:
922                            "Learning rate appears too high - may cause training instability"
923                                .to_string(),
924                        evidence: vec![Evidence {
925                            metric_name: "learning_rate".to_string(),
926                            observed_value: lr,
927                            expected_range: (0.0001, 0.01),
928                            explanation: "Learning rate is above typical range".to_string(),
929                        }],
930                        metrics: HashMap::new(),
931                        detected_at: chrono::Utc::now(),
932                    });
933                } else if lr < 0.00001 {
934                    issues.push(DetectedIssue {
935                        issue_type: IssueType::LearningRateTooLow,
936                        severity: IssueSeverity::Medium,
937                        confidence: 0.6,
938                        description: "Learning rate might be too low - training could be slow"
939                            .to_string(),
940                        evidence: vec![Evidence {
941                            metric_name: "learning_rate".to_string(),
942                            observed_value: lr,
943                            expected_range: (0.0001, 0.01),
944                            explanation: "Learning rate is below typical range".to_string(),
945                        }],
946                        metrics: HashMap::new(),
947                        detected_at: chrono::Utc::now(),
948                    });
949                }
950            }
951        }
952
953        Ok(issues)
954    }
955
956    fn get_detector_name(&self) -> &str {
957        "HyperparameterIssueDetector"
958    }
959
960    fn get_supported_issues(&self) -> Vec<IssueType> {
961        vec![
962            IssueType::LearningRateTooHigh,
963            IssueType::LearningRateTooLow,
964        ]
965    }
966}
967
968#[derive(Debug)]
969struct ArchitectureIssueDetector;
970
971impl ArchitectureIssueDetector {
972    fn new() -> Self {
973        Self
974    }
975}
976
977impl IssueDetector for ArchitectureIssueDetector {
978    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>> {
979        let mut issues = Vec::new();
980
981        if let Some(model_info) = context.model_info {
982            // Check model size
983            if model_info.parameter_count > 1_000_000_000 {
984                issues.push(DetectedIssue {
985                    issue_type: IssueType::ModelTooLarge,
986                    severity: IssueSeverity::Medium,
987                    confidence: 0.6,
988                    description:
989                        "Model has very large number of parameters - consider optimization"
990                            .to_string(),
991                    evidence: vec![Evidence {
992                        metric_name: "parameter_count".to_string(),
993                        observed_value: model_info.parameter_count as f64,
994                        expected_range: (1_000_000.0, 100_000_000.0),
995                        explanation: "Parameter count is extremely high".to_string(),
996                    }],
997                    metrics: HashMap::new(),
998                    detected_at: chrono::Utc::now(),
999                });
1000            }
1001
1002            if model_info.layer_count > 100 {
1003                issues.push(DetectedIssue {
1004                    issue_type: IssueType::InappropriateArchitecture,
1005                    severity: IssueSeverity::Low,
1006                    confidence: 0.5,
1007                    description: "Very deep model - may have gradient flow issues".to_string(),
1008                    evidence: vec![Evidence {
1009                        metric_name: "layer_count".to_string(),
1010                        observed_value: model_info.layer_count as f64,
1011                        expected_range: (10.0, 50.0),
1012                        explanation: "Layer count is very high".to_string(),
1013                    }],
1014                    metrics: HashMap::new(),
1015                    detected_at: chrono::Utc::now(),
1016                });
1017            }
1018        }
1019
1020        Ok(issues)
1021    }
1022
1023    fn get_detector_name(&self) -> &str {
1024        "ArchitectureIssueDetector"
1025    }
1026
1027    fn get_supported_issues(&self) -> Vec<IssueType> {
1028        vec![
1029            IssueType::ModelTooLarge,
1030            IssueType::InappropriateArchitecture,
1031        ]
1032    }
1033}
1034
1035#[derive(Debug)]
1036struct DataIssueDetector;
1037
1038impl DataIssueDetector {
1039    fn new() -> Self {
1040        Self
1041    }
1042}
1043
1044impl IssueDetector for DataIssueDetector {
1045    fn detect_issues(&self, context: &DebugContext) -> Result<Vec<DetectedIssue>> {
1046        // Detect three classes of data-related issues from dashboard metrics:
1047        //
1048        //  - BatchSizeProblems  : sustained low GPU utilisation paired with low
1049        //                         tokens/sec is a strong signal that the batch is
1050        //                         too small to saturate the device.
1051        //  - DataImbalance      : accuracy pinned at a near-trivial value (high
1052        //                         floor or low ceiling) while loss continues to
1053        //                         change is the canonical signature of a model
1054        //                         collapsing onto a majority class.
1055        //  - InsufficientData   : loss decreases but accuracy oscillates wildly,
1056        //                         indicating the model is memorising rather than
1057        //                         generalising — a classic small-dataset failure
1058        //                         mode.
1059        //
1060        // Heuristics use thresholds tuned for a "typical" supervised-learning
1061        // setup; they are intentionally conservative so we do not produce false
1062        // positives on small recent_metrics windows.
1063        let mut issues = Vec::new();
1064
1065        const MIN_WINDOW: usize = 5;
1066        if context.recent_metrics.len() < MIN_WINDOW {
1067            return Ok(issues);
1068        }
1069
1070        // Sample the freshest MIN_WINDOW metrics for analysis.
1071        let window: Vec<&DashboardMetrics> =
1072            context.recent_metrics.iter().rev().take(MIN_WINDOW * 2).collect();
1073
1074        // --- BatchSizeProblems ---------------------------------------------
1075        let gpu_samples: Vec<f64> = window.iter().filter_map(|m| m.gpu_utilization).collect();
1076        let tps_samples: Vec<f64> = window.iter().filter_map(|m| m.tokens_per_second).collect();
1077        if gpu_samples.len() >= MIN_WINDOW && tps_samples.len() >= MIN_WINDOW {
1078            let gpu_mean = gpu_samples.iter().sum::<f64>() / gpu_samples.len() as f64;
1079            let tps_mean = tps_samples.iter().sum::<f64>() / tps_samples.len() as f64;
1080            // Low GPU and low throughput together strongly suggest the batch is
1081            // starving the device. We use 0.5 (50% utilisation) and 100 tok/s as
1082            // canonical thresholds, matching the project's other detectors.
1083            if gpu_mean < 0.5 && tps_mean < 100.0 {
1084                let mut metrics = HashMap::new();
1085                metrics.insert("avg_gpu_utilization".to_string(), gpu_mean);
1086                metrics.insert("avg_tokens_per_second".to_string(), tps_mean);
1087                issues.push(DetectedIssue {
1088                    issue_type: IssueType::BatchSizeProblems,
1089                    severity: IssueSeverity::Medium,
1090                    confidence: 0.7,
1091                    description:
1092                        "Sustained low GPU utilisation and throughput suggest batch size may be \
1093                         too small to saturate the device"
1094                            .to_string(),
1095                    evidence: vec![
1096                        Evidence {
1097                            metric_name: "gpu_utilization".to_string(),
1098                            observed_value: gpu_mean,
1099                            expected_range: (0.7, 1.0),
1100                            explanation:
1101                                "Average GPU utilisation is below the typical training range"
1102                                    .to_string(),
1103                        },
1104                        Evidence {
1105                            metric_name: "tokens_per_second".to_string(),
1106                            observed_value: tps_mean,
1107                            expected_range: (100.0, f64::INFINITY),
1108                            explanation: "Throughput is below the typical training floor"
1109                                .to_string(),
1110                        },
1111                    ],
1112                    metrics,
1113                    detected_at: chrono::Utc::now(),
1114                });
1115            }
1116        }
1117
1118        // --- DataImbalance / InsufficientData ------------------------------
1119        let acc_samples: Vec<f64> = window.iter().filter_map(|m| m.accuracy).collect();
1120        let loss_samples: Vec<f64> = window.iter().filter_map(|m| m.loss).collect();
1121
1122        if acc_samples.len() >= MIN_WINDOW && loss_samples.len() >= MIN_WINDOW {
1123            let acc_mean = acc_samples.iter().sum::<f64>() / acc_samples.len() as f64;
1124            let acc_var = acc_samples
1125                .iter()
1126                .map(|a| {
1127                    let d = a - acc_mean;
1128                    d * d
1129                })
1130                .sum::<f64>()
1131                / acc_samples.len() as f64;
1132            let acc_stddev = acc_var.sqrt();
1133
1134            // `window` (and therefore `loss_samples`) is ordered newest-first.
1135            // Compare the older half (end of the slice) to the newer half
1136            // (start of the slice) to decide whether loss is decreasing.
1137            let half = loss_samples.len() / 2;
1138            let newer_half = &loss_samples[..half];
1139            let older_half = &loss_samples[loss_samples.len() - half..];
1140            let newer_avg = if newer_half.is_empty() {
1141                0.0
1142            } else {
1143                newer_half.iter().sum::<f64>() / newer_half.len() as f64
1144            };
1145            let older_avg = if older_half.is_empty() {
1146                0.0
1147            } else {
1148                older_half.iter().sum::<f64>() / older_half.len() as f64
1149            };
1150            // Positive => loss decreasing over time.
1151            let loss_relative_change = if older_avg.abs() > f64::EPSILON {
1152                (older_avg - newer_avg) / older_avg.abs()
1153            } else {
1154                0.0
1155            };
1156
1157            // DataImbalance: accuracy is pinned (very low variance) at an
1158            // extreme value (either trivially low or near-perfect) while the
1159            // loss continues to move meaningfully. Models collapsing onto the
1160            // majority class show exactly this signature.
1161            let acc_pinned_extreme = acc_stddev < 0.01 && !(0.2..=0.95).contains(&acc_mean);
1162            let loss_changing = loss_relative_change.abs() > 0.05;
1163            if acc_pinned_extreme && loss_changing {
1164                let mut metrics = HashMap::new();
1165                metrics.insert("accuracy_mean".to_string(), acc_mean);
1166                metrics.insert("accuracy_stddev".to_string(), acc_stddev);
1167                metrics.insert("loss_relative_change".to_string(), loss_relative_change);
1168                issues.push(DetectedIssue {
1169                    issue_type: IssueType::DataImbalance,
1170                    severity: IssueSeverity::High,
1171                    confidence: 0.75,
1172                    description:
1173                        "Accuracy is pinned at an extreme value while loss continues to change \
1174                         — model may be collapsing onto a majority class"
1175                            .to_string(),
1176                    evidence: vec![Evidence {
1177                        metric_name: "accuracy_stddev".to_string(),
1178                        observed_value: acc_stddev,
1179                        expected_range: (0.01, 0.5),
1180                        explanation:
1181                            "Accuracy variance is far below the range expected during healthy \
1182                             training"
1183                                .to_string(),
1184                    }],
1185                    metrics,
1186                    detected_at: chrono::Utc::now(),
1187                });
1188            }
1189
1190            // InsufficientData: loss is steadily decreasing (model fitting the
1191            // training set) but accuracy is highly volatile, suggesting the
1192            // model is memorising rather than generalising — a classic failure
1193            // mode of training on too little data.
1194            if loss_relative_change > 0.10 && acc_stddev > 0.15 {
1195                let mut metrics = HashMap::new();
1196                metrics.insert("accuracy_stddev".to_string(), acc_stddev);
1197                metrics.insert("loss_relative_change".to_string(), loss_relative_change);
1198                issues.push(DetectedIssue {
1199                    issue_type: IssueType::InsufficientData,
1200                    severity: IssueSeverity::Medium,
1201                    confidence: 0.6,
1202                    description:
1203                        "Loss is decreasing but accuracy fluctuates wildly — the dataset may be \
1204                         too small, leading to memorisation rather than generalisation"
1205                            .to_string(),
1206                    evidence: vec![Evidence {
1207                        metric_name: "accuracy_stddev".to_string(),
1208                        observed_value: acc_stddev,
1209                        expected_range: (0.0, 0.10),
1210                        explanation:
1211                            "Accuracy variance is well above what is expected when the model \
1212                             is generalising"
1213                                .to_string(),
1214                    }],
1215                    metrics,
1216                    detected_at: chrono::Utc::now(),
1217                });
1218            }
1219        }
1220
1221        Ok(issues)
1222    }
1223
1224    fn get_detector_name(&self) -> &str {
1225        "DataIssueDetector"
1226    }
1227
1228    fn get_supported_issues(&self) -> Vec<IssueType> {
1229        vec![
1230            IssueType::DataImbalance,
1231            IssueType::BatchSizeProblems,
1232            IssueType::InsufficientData,
1233        ]
1234    }
1235}
1236
1237impl Default for KnowledgeBase {
1238    fn default() -> Self {
1239        Self::new()
1240    }
1241}
1242
1243impl KnowledgeBase {
1244    pub fn new() -> Self {
1245        Self {
1246            issue_patterns: HashMap::new(),
1247            hyperparameter_recommendations: HashMap::new(),
1248            architecture_patterns: Vec::new(),
1249            best_practices: HashMap::new(),
1250        }
1251    }
1252}
1253
1254// Report structures
1255
1256#[derive(Debug, Serialize, Deserialize)]
1257pub struct AutoDebugReport {
1258    pub detected_issues: Vec<DetectedIssue>,
1259    pub fix_recommendations: Vec<FixRecommendation>,
1260    pub hyperparameter_recommendations: Vec<HyperparameterRecommendation>,
1261    pub architecture_suggestions: Vec<ArchitectureSuggestion>,
1262    pub training_recipe: TrainingRecipeOptimization,
1263    pub analysis_summary: String,
1264    pub confidence_score: f64,
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1268pub struct FixRecommendation {
1269    pub issue: DetectedIssue,
1270    pub fix_suggestion: FixSuggestion,
1271    pub confidence: f64,
1272    pub urgency: f64,
1273}
1274
1275#[derive(Debug, Clone, Serialize, Deserialize)]
1276pub struct HyperparameterRecommendation {
1277    pub parameter: String,
1278    pub current_value: Option<f64>,
1279    pub recommended_value: f64,
1280    pub reason: String,
1281    pub confidence: f64,
1282}
1283
1284#[derive(Debug, Clone, Serialize, Deserialize)]
1285pub struct ArchitectureSuggestion {
1286    pub suggestion_type: String,
1287    pub title: String,
1288    pub description: String,
1289    pub impact_assessment: String,
1290    pub implementation_difficulty: String,
1291}
1292
1293#[derive(Debug, Clone, Serialize, Deserialize)]
1294pub struct TrainingRecipeOptimization {
1295    pub recommended_optimizations: Vec<String>,
1296    pub training_schedule: TrainingSchedule,
1297    pub data_strategy: DataStrategy,
1298}
1299
1300#[derive(Debug, Clone, Serialize, Deserialize)]
1301pub struct TrainingSchedule {
1302    pub warmup_steps: u32,
1303    pub learning_rate_schedule: String,
1304    pub batch_size_schedule: String,
1305    pub early_stopping: bool,
1306    pub checkpoint_frequency: u32,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1310pub struct DataStrategy {
1311    pub data_augmentation: Vec<String>,
1312    pub sampling_strategy: String,
1313    pub preprocessing_optimizations: Vec<String>,
1314}
1315
1316#[cfg(test)]
1317#[path = "auto_debugger_tests.rs"]
1318mod auto_debugger_tests;
1319
1320#[cfg(test)]
1321mod tests {
1322    use super::*;
1323
1324    fn make_config() -> DebugConfig {
1325        DebugConfig::default()
1326    }
1327
1328    #[test]
1329    fn test_knowledge_base_new() {
1330        let kb = KnowledgeBase::new();
1331        assert!(kb.issue_patterns.is_empty());
1332        assert!(kb.hyperparameter_recommendations.is_empty());
1333        assert!(kb.architecture_patterns.is_empty());
1334        assert!(kb.best_practices.is_empty());
1335    }
1336
1337    #[test]
1338    fn test_knowledge_base_default() {
1339        let kb = KnowledgeBase::default();
1340        assert!(kb.issue_patterns.is_empty());
1341    }
1342
1343    #[test]
1344    fn test_auto_debugger_new() {
1345        let config = make_config();
1346        let debugger = AutoDebugger::new(&config);
1347        assert!(!debugger.issue_detectors.is_empty());
1348        assert!(!debugger.fix_suggestions.is_empty());
1349        assert!(debugger.optimization_history.is_empty());
1350    }
1351
1352    #[test]
1353    fn test_auto_debugger_has_default_detectors() {
1354        let config = make_config();
1355        let debugger = AutoDebugger::new(&config);
1356        assert_eq!(debugger.issue_detectors.len(), 6);
1357    }
1358
1359    #[test]
1360    fn test_auto_debugger_has_fix_suggestions() {
1361        let config = make_config();
1362        let debugger = AutoDebugger::new(&config);
1363        assert!(debugger.fix_suggestions.contains_key(&IssueType::VanishingGradients));
1364        assert!(debugger.fix_suggestions.contains_key(&IssueType::ExplodingGradients));
1365    }
1366
1367    #[test]
1368    fn test_gradient_issue_detector_name() {
1369        let detector = GradientIssueDetector::new();
1370        assert_eq!(detector.get_detector_name(), "GradientIssueDetector");
1371    }
1372
1373    #[test]
1374    fn test_gradient_issue_detector_supported_issues() {
1375        let detector = GradientIssueDetector::new();
1376        let issues = detector.get_supported_issues();
1377        assert!(issues.contains(&IssueType::VanishingGradients));
1378        assert!(issues.contains(&IssueType::ExplodingGradients));
1379    }
1380
1381    #[test]
1382    fn test_training_issue_detector_name() {
1383        let detector = TrainingIssueDetector::new();
1384        assert_eq!(detector.get_detector_name(), "TrainingIssueDetector");
1385    }
1386
1387    #[test]
1388    fn test_training_issue_detector_supported_issues() {
1389        let detector = TrainingIssueDetector::new();
1390        let issues = detector.get_supported_issues();
1391        assert!(!issues.is_empty());
1392    }
1393
1394    #[test]
1395    fn test_performance_issue_detector_name() {
1396        let detector = PerformanceIssueDetector::new();
1397        assert_eq!(detector.get_detector_name(), "PerformanceIssueDetector");
1398    }
1399
1400    #[test]
1401    fn test_hyperparameter_issue_detector_name() {
1402        let detector = HyperparameterIssueDetector::new();
1403        assert_eq!(detector.get_detector_name(), "HyperparameterIssueDetector");
1404    }
1405
1406    #[test]
1407    fn test_architecture_issue_detector_name() {
1408        let detector = ArchitectureIssueDetector::new();
1409        assert_eq!(detector.get_detector_name(), "ArchitectureIssueDetector");
1410    }
1411
1412    #[test]
1413    fn test_data_issue_detector_name() {
1414        let detector = DataIssueDetector::new();
1415        assert_eq!(detector.get_detector_name(), "DataIssueDetector");
1416    }
1417
1418    #[test]
1419    fn test_issue_type_equality() {
1420        assert_eq!(IssueType::VanishingGradients, IssueType::VanishingGradients);
1421        assert_ne!(IssueType::VanishingGradients, IssueType::ExplodingGradients);
1422    }
1423
1424    #[test]
1425    fn test_issue_type_hash_compatible() {
1426        let mut map = HashMap::new();
1427        map.insert(IssueType::OverfittingDetected, "fix");
1428        assert!(map.contains_key(&IssueType::OverfittingDetected));
1429        assert!(!map.contains_key(&IssueType::UnderfittingDetected));
1430    }
1431
1432    #[test]
1433    fn test_evidence_construction() {
1434        let evidence = Evidence {
1435            metric_name: "gradient_norm".to_string(),
1436            observed_value: 0.001,
1437            expected_range: (0.01, 1.0),
1438            explanation: "Gradient norm too low".to_string(),
1439        };
1440        assert_eq!(evidence.metric_name, "gradient_norm");
1441        assert!(evidence.observed_value < evidence.expected_range.0);
1442    }
1443
1444    #[test]
1445    fn test_expected_impact_fields() {
1446        let impact = ExpectedImpact {
1447            performance_improvement: 0.15,
1448            training_speed_improvement: 0.05,
1449            stability_improvement: 0.25,
1450            memory_usage_change: 0.02,
1451        };
1452        assert!(impact.performance_improvement > 0.0);
1453        assert!(impact.stability_improvement > impact.performance_improvement);
1454    }
1455
1456    #[test]
1457    fn test_model_info_construction() {
1458        let info = ModelInfo {
1459            model_type: "transformer".to_string(),
1460            parameter_count: 1_000_000,
1461            layer_count: 12,
1462            architecture_details: HashMap::new(),
1463        };
1464        assert_eq!(info.model_type, "transformer");
1465        assert_eq!(info.parameter_count, 1_000_000);
1466    }
1467
1468    #[test]
1469    fn test_issue_pattern_construction() {
1470        let pattern = IssuePattern {
1471            symptoms: vec!["low gradient norm".to_string()],
1472            common_causes: vec!["deep network".to_string()],
1473            diagnostic_metrics: vec!["gradient_norm".to_string()],
1474            typical_solutions: vec!["add skip connections".to_string()],
1475        };
1476        assert_eq!(pattern.symptoms.len(), 1);
1477        assert_eq!(pattern.common_causes.len(), 1);
1478    }
1479
1480    #[test]
1481    fn test_hyperparameter_advice_construction() {
1482        let advice = HyperparameterAdvice {
1483            parameter_name: "learning_rate".to_string(),
1484            recommended_range: (1e-5, 1e-2),
1485            tuning_strategy: "grid_search".to_string(),
1486            dependencies: vec!["batch_size".to_string()],
1487            common_mistakes: vec!["too high initial lr".to_string()],
1488        };
1489        assert!(advice.recommended_range.0 < advice.recommended_range.1);
1490    }
1491
1492    fn make_metric(
1493        loss: Option<f64>,
1494        accuracy: Option<f64>,
1495        gpu: Option<f64>,
1496        tps: Option<f64>,
1497    ) -> DashboardMetrics {
1498        DashboardMetrics {
1499            timestamp: std::time::SystemTime::now(),
1500            loss,
1501            accuracy,
1502            learning_rate: Some(1e-3),
1503            memory_usage_mb: 1024.0,
1504            gpu_utilization: gpu,
1505            tokens_per_second: tps,
1506            gradient_norm: Some(0.5),
1507            epoch: Some(0),
1508            step: Some(0),
1509        }
1510    }
1511
1512    #[test]
1513    fn test_data_issue_detector_returns_empty_with_no_metrics() {
1514        let detector = DataIssueDetector::new();
1515        let context = DebugContext {
1516            profiler_report: None,
1517            gradient_report: None,
1518            anomaly_report: None,
1519            recent_metrics: &[],
1520            training_duration: Duration::from_secs(60),
1521            model_info: None,
1522        };
1523        let issues = detector.detect_issues(&context).expect("detect_issues should succeed");
1524        assert!(issues.is_empty());
1525    }
1526
1527    #[test]
1528    fn test_data_issue_detector_flags_batch_size_problem() {
1529        let detector = DataIssueDetector::new();
1530        // Simulate a long stretch of low GPU utilisation and low throughput.
1531        let metrics: Vec<DashboardMetrics> = (0..10)
1532            .map(|i| {
1533                make_metric(
1534                    Some(2.0 - i as f64 * 0.01),
1535                    Some(0.6),
1536                    Some(0.2),
1537                    Some(50.0),
1538                )
1539            })
1540            .collect();
1541        let context = DebugContext {
1542            profiler_report: None,
1543            gradient_report: None,
1544            anomaly_report: None,
1545            recent_metrics: &metrics,
1546            training_duration: Duration::from_secs(600),
1547            model_info: None,
1548        };
1549        let issues = detector.detect_issues(&context).expect("detect_issues should succeed");
1550        assert!(
1551            issues.iter().any(|i| i.issue_type == IssueType::BatchSizeProblems),
1552            "expected BatchSizeProblems to be flagged, got: {:?}",
1553            issues.iter().map(|i| &i.issue_type).collect::<Vec<_>>()
1554        );
1555    }
1556
1557    #[test]
1558    fn test_data_issue_detector_flags_data_imbalance_when_accuracy_pinned() {
1559        let detector = DataIssueDetector::new();
1560        // Accuracy pinned at ~0.97 with virtually no variance, while loss
1561        // continues to fall: a classic majority-class collapse.
1562        let metrics: Vec<DashboardMetrics> = (0..10)
1563            .map(|i| {
1564                make_metric(
1565                    Some(2.0 - i as f64 * 0.10),
1566                    Some(0.97),
1567                    Some(0.85),
1568                    Some(500.0),
1569                )
1570            })
1571            .collect();
1572        let context = DebugContext {
1573            profiler_report: None,
1574            gradient_report: None,
1575            anomaly_report: None,
1576            recent_metrics: &metrics,
1577            training_duration: Duration::from_secs(600),
1578            model_info: None,
1579        };
1580        let issues = detector.detect_issues(&context).expect("detect_issues should succeed");
1581        assert!(
1582            issues.iter().any(|i| i.issue_type == IssueType::DataImbalance),
1583            "expected DataImbalance to be flagged, got: {:?}",
1584            issues.iter().map(|i| &i.issue_type).collect::<Vec<_>>()
1585        );
1586    }
1587}