1#![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#[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub enum IssueType {
29 VanishingGradients,
31 ExplodingGradients,
32 LearningRateProblems,
33 OverfittingDetected,
34 UnderfittingDetected,
35 TrainingStalled,
36 LossNotDecreasing,
37 UnstableTraining,
38 MemoryIssues,
39
40 ModelTooLarge,
42 ModelTooSmall,
43 InappropriateArchitecture,
44 LayerMismatch,
45 ActivationProblems,
46
47 DataImbalance,
49 DataLeakage,
50 InsufficientData,
51 DataQualityIssues,
52 BatchSizeProblems,
53
54 SlowTraining,
56 LowGpuUtilization,
57 MemoryBottleneck,
58 IoBottleneck,
59 ComputeBottleneck,
60
61 LearningRateTooHigh,
63 LearningRateTooLow,
64 BatchSizeTooLarge,
65 BatchSizeTooSmall,
66 RegularizationIssues,
67}
68
69#[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#[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, Easy, Medium, Hard, Complex, }
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#[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#[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
203pub 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#[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 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 auto_debugger.register_default_detectors();
242 auto_debugger.initialize_fix_suggestions();
243
244 auto_debugger
245 }
246
247 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 fn initialize_fix_suggestions(&mut self) {
259 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 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 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 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 pub fn analyze_issues(&self, context: &DebugContext) -> Result<AutoDebugReport> {
437 let mut all_issues = Vec::new();
438
439 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 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 let fix_recommendations = self.generate_fix_recommendations(&all_issues);
473
474 let hyperparameter_recommendations = self.generate_hyperparameter_recommendations(context);
476
477 let architecture_suggestions = self.generate_architecture_suggestions(context);
479
480 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 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, urgency: self.calculate_urgency(issue),
506 });
507 }
508 }
509 }
510
511 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 fn generate_hyperparameter_recommendations(
539 &self,
540 context: &DebugContext,
541 ) -> Vec<HyperparameterRecommendation> {
542 let mut recommendations = Vec::new();
543
544 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 recommendations
568 }
569
570 fn generate_architecture_suggestions(
572 &self,
573 context: &DebugContext,
574 ) -> Vec<ArchitectureSuggestion> {
575 let mut suggestions = Vec::new();
576
577 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 fn generate_training_recipe_optimization(
608 &self,
609 context: &DebugContext,
610 ) -> TrainingRecipeOptimization {
611 let mut optimizations = Vec::new();
612
613 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 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 pub fn record_optimization_attempt(&mut self, attempt: OptimizationAttempt) {
704 self.optimization_history.push(attempt);
705
706 if self.optimization_history.len() > 1000 {
708 self.optimization_history.drain(0..500);
709 }
710 }
711
712 pub fn get_optimization_history(&self) -> &[OptimizationAttempt] {
714 &self.optimization_history
715 }
716}
717
718#[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 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 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 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 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 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 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 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 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 let window: Vec<&DashboardMetrics> =
1072 context.recent_metrics.iter().rev().take(MIN_WINDOW * 2).collect();
1073
1074 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 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 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 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 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 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 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#[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 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 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}