1#![allow(dead_code)]
6
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant, SystemTime};
12use uuid::Uuid;
13
14use crate::DebugConfig;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DashboardMetrics {
19 pub timestamp: SystemTime,
20 pub loss: Option<f64>,
21 pub accuracy: Option<f64>,
22 pub learning_rate: Option<f64>,
23 pub memory_usage_mb: f64,
24 pub gpu_utilization: Option<f64>,
25 pub tokens_per_second: Option<f64>,
26 pub gradient_norm: Option<f64>,
27 pub epoch: Option<u32>,
28 pub step: Option<u64>,
29}
30
31#[derive(Debug)]
33pub struct TrainingMonitor {
34 config: DebugConfig,
35 metrics_history: VecDeque<DashboardMetrics>,
36 max_history: usize,
37 start_time: Instant,
38 alert_thresholds: AlertThresholds,
39 active_alerts: Vec<TrainingAlert>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AlertThresholds {
45 pub loss_increase_threshold: f64,
46 pub gradient_norm_max: f64,
47 pub memory_usage_max_mb: f64,
48 pub gpu_utilization_min: f64,
49 pub learning_rate_min: f64,
50 pub tokens_per_second_min: f64,
51}
52
53impl Default for AlertThresholds {
54 fn default() -> Self {
55 Self {
56 loss_increase_threshold: 1.5,
57 gradient_norm_max: 10.0,
58 memory_usage_max_mb: 8192.0,
59 gpu_utilization_min: 0.7,
60 learning_rate_min: 1e-8,
61 tokens_per_second_min: 100.0,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct TrainingAlert {
69 pub alert_type: AlertType,
70 pub severity: AlertSeverity,
71 pub message: String,
72 pub timestamp: SystemTime,
73 pub metric_value: f64,
74 pub threshold: f64,
75 pub suggested_action: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub enum AlertType {
80 LossIncrease,
81 GradientExplosion,
82 MemoryOveruse,
83 LowGpuUtilization,
84 LearningRateTooLow,
85 SlowTokenProcessing,
86 ModelDivergence,
87 TrainingStalled,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub enum AlertSeverity {
92 Info,
93 Warning,
94 Critical,
95}
96
97impl TrainingMonitor {
98 pub fn new(config: &DebugConfig) -> Self {
100 Self {
101 config: config.clone(),
102 metrics_history: VecDeque::new(),
103 max_history: 10000,
104 start_time: Instant::now(),
105 alert_thresholds: AlertThresholds::default(),
106 active_alerts: Vec::new(),
107 }
108 }
109
110 pub fn update_metrics(&mut self, metrics: DashboardMetrics) {
112 self.metrics_history.push_back(metrics.clone());
114
115 if self.metrics_history.len() > self.max_history {
117 self.metrics_history.pop_front();
118 }
119
120 self.check_alerts(&metrics);
122 }
123
124 pub fn get_recent_metrics(&self, count: usize) -> Vec<DashboardMetrics> {
126 self.metrics_history.iter().rev().take(count).rev().cloned().collect()
127 }
128
129 pub fn get_active_alerts(&self) -> &[TrainingAlert] {
131 &self.active_alerts
132 }
133
134 pub fn clear_alert(&mut self, alert_type: AlertType) {
146 self.active_alerts.retain(|alert| alert.alert_type != alert_type);
147 }
148
149 pub fn set_alert_thresholds(&mut self, thresholds: AlertThresholds) {
151 self.alert_thresholds = thresholds;
152 }
153
154 pub fn generate_training_summary(&self) -> TrainingSummary {
156 let total_duration = self.start_time.elapsed();
157 let total_steps = self.metrics_history.len();
158
159 let avg_loss = self.calculate_average_loss();
160 let best_accuracy = self.calculate_best_accuracy();
161 let avg_tokens_per_second = self.calculate_average_tokens_per_second();
162 let training_stability = self.calculate_training_stability();
163
164 TrainingSummary {
165 total_duration,
166 total_steps,
167 avg_loss,
168 best_accuracy,
169 avg_tokens_per_second,
170 training_stability,
171 active_alerts_count: self.active_alerts.len(),
172 convergence_status: self.assess_convergence(),
173 }
174 }
175
176 fn check_alerts(&mut self, metrics: &DashboardMetrics) {
177 if let Some(current_loss) = metrics.loss {
179 if let Some(prev_metrics) =
180 self.metrics_history.get(self.metrics_history.len().saturating_sub(10))
181 {
182 if let Some(prev_loss) = prev_metrics.loss {
183 if current_loss > prev_loss * self.alert_thresholds.loss_increase_threshold {
184 self.add_alert(TrainingAlert {
185 alert_type: AlertType::LossIncrease,
186 severity: AlertSeverity::Warning,
187 message: "Loss has increased significantly".to_string(),
188 timestamp: SystemTime::now(),
189 metric_value: current_loss,
190 threshold: prev_loss * self.alert_thresholds.loss_increase_threshold,
191 suggested_action: "Check learning rate or data quality".to_string(),
192 });
193 }
194 }
195 }
196 }
197
198 if let Some(grad_norm) = metrics.gradient_norm {
200 if grad_norm > self.alert_thresholds.gradient_norm_max {
201 self.add_alert(TrainingAlert {
202 alert_type: AlertType::GradientExplosion,
203 severity: AlertSeverity::Critical,
204 message: "Gradient explosion detected".to_string(),
205 timestamp: SystemTime::now(),
206 metric_value: grad_norm,
207 threshold: self.alert_thresholds.gradient_norm_max,
208 suggested_action: "Apply gradient clipping or reduce learning rate".to_string(),
209 });
210 }
211 }
212
213 if metrics.memory_usage_mb > self.alert_thresholds.memory_usage_max_mb {
215 self.add_alert(TrainingAlert {
216 alert_type: AlertType::MemoryOveruse,
217 severity: AlertSeverity::Warning,
218 message: "High memory usage detected".to_string(),
219 timestamp: SystemTime::now(),
220 metric_value: metrics.memory_usage_mb,
221 threshold: self.alert_thresholds.memory_usage_max_mb,
222 suggested_action: "Reduce batch size or enable gradient checkpointing".to_string(),
223 });
224 }
225
226 if let Some(gpu_util) = metrics.gpu_utilization {
228 if gpu_util < self.alert_thresholds.gpu_utilization_min {
229 self.add_alert(TrainingAlert {
230 alert_type: AlertType::LowGpuUtilization,
231 severity: AlertSeverity::Info,
232 message: "Low GPU utilization".to_string(),
233 timestamp: SystemTime::now(),
234 metric_value: gpu_util,
235 threshold: self.alert_thresholds.gpu_utilization_min,
236 suggested_action: "Increase batch size or check data loading".to_string(),
237 });
238 }
239 }
240
241 if let Some(tps) = metrics.tokens_per_second {
243 if tps < self.alert_thresholds.tokens_per_second_min {
244 self.add_alert(TrainingAlert {
245 alert_type: AlertType::SlowTokenProcessing,
246 severity: AlertSeverity::Warning,
247 message: "Slow token processing detected".to_string(),
248 timestamp: SystemTime::now(),
249 metric_value: tps,
250 threshold: self.alert_thresholds.tokens_per_second_min,
251 suggested_action: "Optimize model or increase batch size".to_string(),
252 });
253 }
254 }
255 }
256
257 fn add_alert(&mut self, alert: TrainingAlert) {
258 if !self.active_alerts.iter().any(|a| a.alert_type == alert.alert_type) {
260 self.active_alerts.push(alert);
261 }
262 }
263
264 fn calculate_average_loss(&self) -> Option<f64> {
265 let losses: Vec<f64> = self.metrics_history.iter().filter_map(|m| m.loss).collect();
266
267 if losses.is_empty() {
268 None
269 } else {
270 Some(losses.iter().sum::<f64>() / losses.len() as f64)
271 }
272 }
273
274 fn calculate_best_accuracy(&self) -> Option<f64> {
275 self.metrics_history
276 .iter()
277 .filter_map(|m| m.accuracy)
278 .fold(None, |acc, x| match acc {
279 None => Some(x),
280 Some(y) => Some(x.max(y)),
281 })
282 }
283
284 fn calculate_average_tokens_per_second(&self) -> Option<f64> {
285 let tps_values: Vec<f64> =
286 self.metrics_history.iter().filter_map(|m| m.tokens_per_second).collect();
287
288 if tps_values.is_empty() {
289 None
290 } else {
291 Some(tps_values.iter().sum::<f64>() / tps_values.len() as f64)
292 }
293 }
294
295 fn calculate_training_stability(&self) -> TrainingStability {
296 if self.metrics_history.len() < 10 {
297 return TrainingStability::Insufficient;
298 }
299
300 let recent_losses: Vec<f64> =
301 self.metrics_history.iter().rev().take(50).filter_map(|m| m.loss).collect();
302
303 if recent_losses.len() < 10 {
304 return TrainingStability::Insufficient;
305 }
306
307 let mean_loss = recent_losses.iter().sum::<f64>() / recent_losses.len() as f64;
309 let variance = recent_losses.iter().map(|&x| (x - mean_loss).powi(2)).sum::<f64>()
310 / recent_losses.len() as f64;
311
312 let std_dev = variance.sqrt();
313 let coefficient_of_variation = if mean_loss != 0.0 { std_dev / mean_loss } else { 0.0 };
314
315 match coefficient_of_variation {
316 cv if cv < 0.1 => TrainingStability::Stable,
317 cv if cv < 0.3 => TrainingStability::Moderate,
318 _ => TrainingStability::Unstable,
319 }
320 }
321
322 fn assess_convergence(&self) -> ConvergenceStatus {
323 if self.metrics_history.len() < 50 {
324 return ConvergenceStatus::TooEarly;
325 }
326
327 let recent_losses: Vec<f64> =
328 self.metrics_history.iter().rev().take(100).filter_map(|m| m.loss).collect();
329
330 if recent_losses.len() < 50 {
331 return ConvergenceStatus::TooEarly;
332 }
333
334 let first_half_avg =
336 recent_losses[25..].iter().sum::<f64>() / (recent_losses.len() - 25) as f64;
337 let second_half_avg = recent_losses[..25].iter().sum::<f64>() / 25.0;
338
339 if second_half_avg < first_half_avg * 0.95 {
340 ConvergenceStatus::Converging
341 } else if (second_half_avg - first_half_avg).abs() / first_half_avg < 0.01 {
342 ConvergenceStatus::Converged
343 } else {
344 ConvergenceStatus::Diverging
345 }
346 }
347}
348
349#[derive(Debug)]
351pub struct ModelComparator {
352 models: HashMap<String, ModelMetrics>,
353 comparison_config: ComparisonConfig,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct ModelMetrics {
358 pub model_id: String,
359 pub model_name: String,
360 pub metrics_history: Vec<DashboardMetrics>,
361 pub final_loss: Option<f64>,
362 pub final_accuracy: Option<f64>,
363 pub training_time: Duration,
364 pub parameter_count: usize,
365 pub model_size_mb: f64,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct ComparisonConfig {
370 pub primary_metric: String,
371 pub comparison_window: usize,
372 pub significance_threshold: f64,
373}
374
375impl Default for ComparisonConfig {
376 fn default() -> Self {
377 Self {
378 primary_metric: "loss".to_string(),
379 comparison_window: 100,
380 significance_threshold: 0.05,
381 }
382 }
383}
384
385impl ModelComparator {
386 pub fn new() -> Self {
388 Self {
389 models: HashMap::new(),
390 comparison_config: ComparisonConfig::default(),
391 }
392 }
393
394 pub fn add_model(&mut self, model_metrics: ModelMetrics) {
396 self.models.insert(model_metrics.model_id.clone(), model_metrics);
397 }
398
399 pub fn compare_models(&self) -> ModelComparisonReport {
401 let mut comparisons = Vec::new();
402 let model_ids: Vec<String> = self.models.keys().cloned().collect();
403
404 for i in 0..model_ids.len() {
405 for j in (i + 1)..model_ids.len() {
406 let model_a = &self.models[&model_ids[i]];
407 let model_b = &self.models[&model_ids[j]];
408
409 let comparison = self.compare_two_models(model_a, model_b);
410 comparisons.push(comparison);
411 }
412 }
413
414 let best_model = self.find_best_model();
415 let ranking = self.rank_models();
416
417 ModelComparisonReport {
418 comparisons,
419 best_model,
420 ranking,
421 comparison_config: self.comparison_config.clone(),
422 }
423 }
424
425 fn compare_two_models(
426 &self,
427 model_a: &ModelMetrics,
428 model_b: &ModelMetrics,
429 ) -> ModelComparison {
430 let performance_diff = self.calculate_performance_difference(model_a, model_b);
431 let efficiency_diff = self.calculate_efficiency_difference(model_a, model_b);
432 let statistical_significance = self.test_statistical_significance(model_a, model_b);
433
434 ModelComparison {
435 model_a_id: model_a.model_id.clone(),
436 model_b_id: model_b.model_id.clone(),
437 performance_difference: performance_diff,
438 efficiency_difference: efficiency_diff,
439 statistical_significance,
440 recommendation: self.generate_recommendation(model_a, model_b, performance_diff),
441 }
442 }
443
444 fn calculate_performance_difference(
453 &self,
454 model_a: &ModelMetrics,
455 model_b: &ModelMetrics,
456 ) -> Option<f64> {
457 match self.comparison_config.primary_metric.as_str() {
458 "loss" => {
459 let (loss_a, loss_b) = (model_a.final_loss?, model_b.final_loss?);
460 if loss_a == 0.0 {
461 return None;
462 }
463 Some((loss_b - loss_a) / loss_a) },
465 "accuracy" => {
466 let (acc_a, acc_b) = (model_a.final_accuracy?, model_b.final_accuracy?);
467 if acc_a == 0.0 {
468 return None;
469 }
470 Some((acc_b - acc_a) / acc_a) },
472 _ => None,
475 }
476 }
477
478 fn calculate_efficiency_difference(
487 &self,
488 model_a: &ModelMetrics,
489 model_b: &ModelMetrics,
490 ) -> Option<f64> {
491 let time_a = model_a.training_time.as_secs_f64();
492 if time_a == 0.0 || model_a.model_size_mb == 0.0 {
493 return None;
494 }
495
496 let time_diff = model_b.training_time.as_secs_f64() / time_a - 1.0;
498
499 let size_diff = model_b.model_size_mb / model_a.model_size_mb - 1.0;
501
502 Some((time_diff + size_diff) / 2.0)
504 }
505
506 fn metric_samples(&self, model: &ModelMetrics) -> Vec<f64> {
513 match self.comparison_config.primary_metric.as_str() {
514 "loss" => model.metrics_history.iter().filter_map(|m| m.loss).collect(),
515 "accuracy" => model.metrics_history.iter().filter_map(|m| m.accuracy).collect(),
516 _ => Vec::new(),
517 }
518 }
519
520 fn test_statistical_significance(
531 &self,
532 model_a: &ModelMetrics,
533 model_b: &ModelMetrics,
534 ) -> Option<bool> {
535 let samples_a = self.metric_samples(model_a);
536 let samples_b = self.metric_samples(model_b);
537 crate::differential_debugging::welch_t_test(
538 &samples_a,
539 &samples_b,
540 self.comparison_config.significance_threshold,
541 )
542 .map(|result| result.is_significant)
543 }
544
545 fn generate_recommendation(
550 &self,
551 model_a: &ModelMetrics,
552 model_b: &ModelMetrics,
553 perf_diff: Option<f64>,
554 ) -> String {
555 let Some(perf_diff) = perf_diff else {
556 return format!(
557 "Cannot compare {} and {}: the '{}' metric was not recorded for both models",
558 model_a.model_name, model_b.model_name, self.comparison_config.primary_metric
559 );
560 };
561 if perf_diff.abs() < 0.01 {
562 "Models perform similarly - choose based on other factors".to_string()
563 } else if perf_diff < 0.0 {
564 format!(
565 "Model {} performs {:.1}% better",
566 model_a.model_name,
567 perf_diff.abs() * 100.0
568 )
569 } else {
570 format!(
571 "Model {} performs {:.1}% better",
572 model_b.model_name,
573 perf_diff * 100.0
574 )
575 }
576 }
577
578 fn find_best_model(&self) -> Option<String> {
579 let mut best_model = None;
580 let mut best_score = f64::NEG_INFINITY;
581
582 for model in self.models.values() {
583 let score = match self.comparison_config.primary_metric.as_str() {
584 "loss" => model.final_loss.map(|l| -l).unwrap_or(f64::NEG_INFINITY),
585 "accuracy" => model.final_accuracy.unwrap_or(0.0),
586 _ => 0.0,
587 };
588
589 if score > best_score {
590 best_score = score;
591 best_model = Some(model.model_id.clone());
592 }
593 }
594
595 best_model
596 }
597
598 fn rank_models(&self) -> Vec<ModelRanking> {
599 let mut rankings: Vec<ModelRanking> = self
600 .models
601 .values()
602 .map(|model| {
603 let score = match self.comparison_config.primary_metric.as_str() {
604 "loss" => model.final_loss.map(|l| -l).unwrap_or(f64::NEG_INFINITY),
605 "accuracy" => model.final_accuracy.unwrap_or(0.0),
606 _ => 0.0,
607 };
608
609 ModelRanking {
610 model_id: model.model_id.clone(),
611 model_name: model.model_name.clone(),
612 score,
613 rank: 0, }
615 })
616 .collect();
617
618 rankings.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
619
620 for (i, ranking) in rankings.iter_mut().enumerate() {
621 ranking.rank = i + 1;
622 }
623
624 rankings
625 }
626}
627
628#[derive(Debug)]
630pub struct HyperparameterExplorer {
631 experiments: HashMap<String, HyperparameterExperiment>,
632 search_space: HyperparameterSearchSpace,
633 optimization_history: Vec<OptimizationStep>,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
637pub struct HyperparameterExperiment {
638 pub experiment_id: String,
639 pub hyperparameters: HashMap<String, HyperparameterValue>,
640 pub results: ExperimentResults,
641 pub status: ExperimentStatus,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
645pub enum HyperparameterValue {
646 Float(f64),
647 Integer(i64),
648 String(String),
649 Boolean(bool),
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct ExperimentResults {
654 pub final_loss: Option<f64>,
655 pub final_accuracy: Option<f64>,
656 pub training_time: Duration,
657 pub convergence_epoch: Option<u32>,
658 pub best_validation_score: Option<f64>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub enum ExperimentStatus {
663 Running,
664 Completed,
665 Failed,
666 Cancelled,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct HyperparameterSearchSpace {
671 pub learning_rate: (f64, f64),
672 pub batch_size: (i64, i64),
673 pub dropout_rate: (f64, f64),
674 pub weight_decay: (f64, f64),
675 pub num_layers: (i64, i64),
676 pub hidden_size: (i64, i64),
677}
678
679impl Default for HyperparameterSearchSpace {
680 fn default() -> Self {
681 Self {
682 learning_rate: (1e-5, 1e-1),
683 batch_size: (4, 128),
684 dropout_rate: (0.0, 0.5),
685 weight_decay: (0.0, 1e-2),
686 num_layers: (1, 12),
687 hidden_size: (64, 2048),
688 }
689 }
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct OptimizationStep {
694 pub step: usize,
695 pub best_experiment_id: String,
696 pub best_score: f64,
697 pub exploration_count: usize,
698 pub exploitation_count: usize,
699}
700
701impl HyperparameterExplorer {
702 pub fn new() -> Self {
704 Self {
705 experiments: HashMap::new(),
706 search_space: HyperparameterSearchSpace::default(),
707 optimization_history: Vec::new(),
708 }
709 }
710
711 pub fn add_experiment(&mut self, experiment: HyperparameterExperiment) {
713 self.experiments.insert(experiment.experiment_id.clone(), experiment);
714 }
715
716 pub fn get_recommendations(&self) -> HyperparameterRecommendations {
718 let best_experiments = self.find_best_experiments(5);
719 let parameter_importance = self.analyze_parameter_importance();
720 let suggested_ranges = self.suggest_search_ranges();
721 let next_experiments = self.suggest_next_experiments(3);
722
723 HyperparameterRecommendations {
724 best_experiments,
725 parameter_importance,
726 suggested_ranges,
727 next_experiments,
728 total_experiments: self.experiments.len(),
729 }
730 }
731
732 fn find_best_experiments(&self, limit: usize) -> Vec<String> {
733 let mut experiments: Vec<_> = self.experiments.values().collect();
734 experiments.sort_by(|a, b| {
735 let score_a = a.results.final_loss.unwrap_or(f64::INFINITY);
736 let score_b = b.results.final_loss.unwrap_or(f64::INFINITY);
737 score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal)
738 });
739
740 experiments.iter().take(limit).map(|exp| exp.experiment_id.clone()).collect()
741 }
742
743 fn analyze_parameter_importance(&self) -> HashMap<String, f64> {
744 let mut importance = HashMap::new();
746 importance.insert("learning_rate".to_string(), 0.8);
747 importance.insert("batch_size".to_string(), 0.6);
748 importance.insert("dropout_rate".to_string(), 0.4);
749 importance.insert("weight_decay".to_string(), 0.3);
750 importance
751 }
752
753 fn suggest_search_ranges(&self) -> HashMap<String, (f64, f64)> {
754 let mut ranges = HashMap::new();
756 ranges.insert("learning_rate".to_string(), (1e-4, 1e-2));
757 ranges.insert("dropout_rate".to_string(), (0.1, 0.3));
758 ranges
759 }
760
761 fn suggest_next_experiments(&self, count: usize) -> Vec<HashMap<String, HyperparameterValue>> {
762 let mut suggestions = Vec::new();
763
764 for i in 0..count {
765 let mut params = HashMap::new();
766
767 params.insert(
769 "learning_rate".to_string(),
770 HyperparameterValue::Float(0.001 * (1.0 + i as f64 * 0.5)),
771 );
772 params.insert(
773 "batch_size".to_string(),
774 HyperparameterValue::Integer(32 * (1 + i as i64)),
775 );
776 params.insert(
777 "dropout_rate".to_string(),
778 HyperparameterValue::Float(0.1 + i as f64 * 0.1),
779 );
780
781 suggestions.push(params);
782 }
783
784 suggestions
785 }
786}
787
788#[derive(Debug)]
790pub struct InteractiveDashboard {
791 config: DebugConfig,
792 training_monitor: TrainingMonitor,
793 model_comparator: ModelComparator,
794 hyperparameter_explorer: HyperparameterExplorer,
795 dashboard_state: DashboardState,
796 websocket_server: Option<DashboardEndpoint>,
797}
798
799#[derive(Debug, Serialize, Deserialize)]
800pub struct DashboardState {
801 pub active_session_id: Option<Uuid>,
802 pub refresh_rate_ms: u64,
803 pub auto_alerts: bool,
804 pub display_mode: DisplayMode,
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
808pub enum DisplayMode {
809 Overview,
810 DetailedMetrics,
811 ModelComparison,
812 HyperparameterOptimization,
813 AlertsOnly,
814}
815
816#[derive(Debug)]
818pub struct DashboardEndpoint {
826 port: u16,
828 connected_clients: Arc<Mutex<Vec<String>>>,
830 pending_updates: Arc<Mutex<VecDeque<DashboardMetrics>>>,
833}
834
835impl DashboardEndpoint {
836 pub fn port(&self) -> u16 {
838 self.port
839 }
840
841 pub fn connected_clients(&self) -> Vec<String> {
843 self.connected_clients.lock().map(|clients| clients.clone()).unwrap_or_default()
844 }
845}
846
847const MAX_PENDING_DASHBOARD_UPDATES: usize = 1000;
849
850impl InteractiveDashboard {
851 pub fn new(config: &DebugConfig) -> Self {
853 Self {
854 config: config.clone(),
855 training_monitor: TrainingMonitor::new(config),
856 model_comparator: ModelComparator::new(),
857 hyperparameter_explorer: HyperparameterExplorer::new(),
858 dashboard_state: DashboardState {
859 active_session_id: None,
860 refresh_rate_ms: 1000,
861 auto_alerts: true,
862 display_mode: DisplayMode::Overview,
863 },
864 websocket_server: None,
865 }
866 }
867
868 pub async fn start(&mut self, port: Option<u16>) -> Result<()> {
870 let port = port.unwrap_or(8080);
871
872 self.websocket_server = Some(DashboardEndpoint {
873 port,
874 connected_clients: Arc::new(Mutex::new(Vec::new())),
875 pending_updates: Arc::new(Mutex::new(VecDeque::new())),
876 });
877
878 tracing::info!(
880 port,
881 "interactive dashboard activated; updates will be queued for the embedding \
882 application to deliver"
883 );
884 Ok(())
885 }
886
887 pub fn drain_pending_updates(&self) -> Vec<DashboardMetrics> {
891 let Some(endpoint) = self.websocket_server.as_ref() else {
892 return Vec::new();
893 };
894 endpoint
895 .pending_updates
896 .lock()
897 .map(|mut queue| queue.drain(..).collect())
898 .unwrap_or_default()
899 }
900
901 pub fn update(&mut self, metrics: DashboardMetrics) {
903 self.training_monitor.update_metrics(metrics.clone());
904
905 self.queue_update(metrics);
908 }
909
910 pub fn get_dashboard_snapshot(&self) -> DashboardSnapshot {
912 let training_summary = self.training_monitor.generate_training_summary();
913 let recent_metrics = self.training_monitor.get_recent_metrics(100);
914 let active_alerts = self.training_monitor.get_active_alerts().to_vec();
915 let model_comparison = self.model_comparator.compare_models();
916 let hyperparameter_recommendations = self.hyperparameter_explorer.get_recommendations();
917
918 DashboardSnapshot {
919 timestamp: SystemTime::now(),
920 training_summary,
921 recent_metrics,
922 active_alerts,
923 model_comparison,
924 hyperparameter_recommendations,
925 dashboard_state: DashboardState {
926 active_session_id: self.dashboard_state.active_session_id,
927 refresh_rate_ms: self.dashboard_state.refresh_rate_ms,
928 auto_alerts: self.dashboard_state.auto_alerts,
929 display_mode: self.dashboard_state.display_mode.clone(),
930 },
931 }
932 }
933
934 pub async fn export_dashboard_data(&self, path: &str) -> Result<()> {
936 let snapshot = self.get_dashboard_snapshot();
937 let json = serde_json::to_string_pretty(&snapshot)?;
938 tokio::fs::write(path, json).await?;
939 Ok(())
940 }
941
942 fn queue_update(&self, metrics: DashboardMetrics) {
948 let Some(endpoint) = self.websocket_server.as_ref() else {
949 return;
950 };
951 if let Ok(mut queue) = endpoint.pending_updates.lock() {
952 queue.push_back(metrics);
953 while queue.len() > MAX_PENDING_DASHBOARD_UPDATES {
954 queue.pop_front();
955 }
956 }
957 }
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize)]
963pub struct TrainingSummary {
964 pub total_duration: Duration,
965 pub total_steps: usize,
966 pub avg_loss: Option<f64>,
967 pub best_accuracy: Option<f64>,
968 pub avg_tokens_per_second: Option<f64>,
969 pub training_stability: TrainingStability,
970 pub active_alerts_count: usize,
971 pub convergence_status: ConvergenceStatus,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize)]
975pub enum TrainingStability {
976 Stable,
977 Moderate,
978 Unstable,
979 Insufficient,
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
983pub enum ConvergenceStatus {
984 TooEarly,
985 Converging,
986 Converged,
987 Diverging,
988}
989
990#[derive(Debug, Serialize, Deserialize)]
991pub struct ModelComparisonReport {
992 pub comparisons: Vec<ModelComparison>,
993 pub best_model: Option<String>,
994 pub ranking: Vec<ModelRanking>,
995 pub comparison_config: ComparisonConfig,
996}
997
998#[derive(Debug, Serialize, Deserialize)]
999pub struct ModelComparison {
1000 pub model_a_id: String,
1001 pub model_b_id: String,
1002 pub performance_difference: Option<f64>,
1007 pub efficiency_difference: Option<f64>,
1010 pub statistical_significance: Option<bool>,
1016 pub recommendation: String,
1017}
1018
1019#[derive(Debug, Serialize, Deserialize)]
1020pub struct ModelRanking {
1021 pub model_id: String,
1022 pub model_name: String,
1023 pub score: f64,
1024 pub rank: usize,
1025}
1026
1027#[derive(Debug, Serialize, Deserialize)]
1028pub struct HyperparameterRecommendations {
1029 pub best_experiments: Vec<String>,
1030 pub parameter_importance: HashMap<String, f64>,
1031 pub suggested_ranges: HashMap<String, (f64, f64)>,
1032 pub next_experiments: Vec<HashMap<String, HyperparameterValue>>,
1033 pub total_experiments: usize,
1034}
1035
1036#[derive(Debug, Serialize, Deserialize)]
1037pub struct DashboardSnapshot {
1038 pub timestamp: SystemTime,
1039 pub training_summary: TrainingSummary,
1040 pub recent_metrics: Vec<DashboardMetrics>,
1041 pub active_alerts: Vec<TrainingAlert>,
1042 pub model_comparison: ModelComparisonReport,
1043 pub hyperparameter_recommendations: HyperparameterRecommendations,
1044 pub dashboard_state: DashboardState,
1045}
1046
1047#[derive(Debug, Serialize, Deserialize)]
1049pub struct DashboardReport {
1050 pub session_duration: Duration,
1051 pub total_metrics_recorded: usize,
1052 pub alerts_triggered: usize,
1053 pub models_compared: usize,
1054 pub experiments_tracked: usize,
1055 pub performance_summary: TrainingSummary,
1056 pub key_insights: Vec<String>,
1057 pub recommendations: Vec<String>,
1058}
1059
1060impl InteractiveDashboard {
1061 pub async fn generate_report(&self) -> Result<DashboardReport> {
1063 let training_summary = self.training_monitor.generate_training_summary();
1064 let total_metrics = self.training_monitor.metrics_history.len();
1065 let alerts_count = self.training_monitor.active_alerts.len();
1066 let models_count = self.model_comparator.models.len();
1067 let experiments_count = self.hyperparameter_explorer.experiments.len();
1068
1069 let key_insights = self.generate_key_insights();
1070 let recommendations = self.generate_recommendations();
1071
1072 Ok(DashboardReport {
1073 session_duration: training_summary.total_duration,
1074 total_metrics_recorded: total_metrics,
1075 alerts_triggered: alerts_count,
1076 models_compared: models_count,
1077 experiments_tracked: experiments_count,
1078 performance_summary: training_summary,
1079 key_insights,
1080 recommendations,
1081 })
1082 }
1083
1084 fn generate_key_insights(&self) -> Vec<String> {
1085 let mut insights = Vec::new();
1086
1087 match self.training_monitor.generate_training_summary().training_stability {
1089 TrainingStability::Stable => insights.push("Training is proceeding stably".to_string()),
1090 TrainingStability::Unstable => insights.push(
1091 "Training shows high variance - consider adjusting hyperparameters".to_string(),
1092 ),
1093 _ => {},
1094 }
1095
1096 if self.model_comparator.models.len() > 1 {
1098 let comparison = self.model_comparator.compare_models();
1099 if let Some(best_model) = comparison.best_model {
1100 insights.push(format!("Best performing model: {}", best_model));
1101 }
1102 }
1103
1104 let critical_alerts = self
1106 .training_monitor
1107 .active_alerts
1108 .iter()
1109 .filter(|alert| matches!(alert.severity, AlertSeverity::Critical))
1110 .count();
1111
1112 if critical_alerts > 0 {
1113 insights.push(format!(
1114 "{} critical alerts require immediate attention",
1115 critical_alerts
1116 ));
1117 }
1118
1119 insights
1120 }
1121
1122 fn generate_recommendations(&self) -> Vec<String> {
1123 let mut recommendations = Vec::new();
1124
1125 for alert in &self.training_monitor.active_alerts {
1127 if matches!(alert.severity, AlertSeverity::Critical) {
1128 recommendations.push(alert.suggested_action.clone());
1129 }
1130 }
1131
1132 if self.hyperparameter_explorer.experiments.len() > 5 {
1134 recommendations.push(
1135 "Continue hyperparameter optimization with narrowed search ranges".to_string(),
1136 );
1137 }
1138
1139 if self.model_comparator.models.len() > 1 {
1141 recommendations
1142 .push("Focus on the best performing model architecture for production".to_string());
1143 }
1144
1145 if recommendations.is_empty() {
1146 recommendations.push("Continue monitoring training progress".to_string());
1147 }
1148
1149 recommendations
1150 }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156
1157 #[tokio::test]
1160 async fn dashboard_updates_are_really_queued_for_delivery() {
1161 let config = DebugConfig::default();
1162 let mut dashboard = InteractiveDashboard::new(&config);
1163 dashboard.update(make_metrics_simple());
1165 assert!(dashboard.drain_pending_updates().is_empty());
1166
1167 dashboard.start(Some(9_999)).await.expect("start");
1168 dashboard.update(make_metrics_simple());
1169 dashboard.update(make_metrics_simple());
1170
1171 let drained = dashboard.drain_pending_updates();
1172 assert_eq!(drained.len(), 2, "both updates must really be retained");
1173 assert!(
1174 dashboard.drain_pending_updates().is_empty(),
1175 "draining must consume the queue"
1176 );
1177 }
1178
1179 #[tokio::test]
1180 async fn the_pending_update_queue_is_bounded() {
1181 let config = DebugConfig::default();
1182 let mut dashboard = InteractiveDashboard::new(&config);
1183 dashboard.start(None).await.expect("start");
1184 for _ in 0..(MAX_PENDING_DASHBOARD_UPDATES + 50) {
1185 dashboard.update(make_metrics_simple());
1186 }
1187 assert_eq!(
1188 dashboard.drain_pending_updates().len(),
1189 MAX_PENDING_DASHBOARD_UPDATES,
1190 "the oldest updates must be dropped, not accumulated without bound"
1191 );
1192 }
1193
1194 fn make_config() -> DebugConfig {
1195 DebugConfig::default()
1196 }
1197
1198 fn make_metrics_with(
1199 loss: Option<f64>,
1200 accuracy: Option<f64>,
1201 memory_mb: f64,
1202 ) -> DashboardMetrics {
1203 DashboardMetrics {
1204 timestamp: SystemTime::now(),
1205 loss,
1206 accuracy,
1207 learning_rate: Some(0.001),
1208 memory_usage_mb: memory_mb,
1209 gpu_utilization: Some(0.8),
1210 tokens_per_second: Some(200.0),
1211 gradient_norm: Some(1.0),
1212 epoch: Some(1),
1213 step: Some(100),
1214 }
1215 }
1216
1217 fn make_metrics_simple() -> DashboardMetrics {
1218 make_metrics_with(Some(0.5), Some(0.85), 2048.0)
1219 }
1220
1221 #[test]
1224 fn test_alert_thresholds_default() {
1225 let thresholds = AlertThresholds::default();
1226 assert!((thresholds.loss_increase_threshold - 1.5).abs() < 1e-9);
1227 assert!((thresholds.gradient_norm_max - 10.0).abs() < 1e-9);
1228 assert!((thresholds.memory_usage_max_mb - 8192.0).abs() < 1e-9);
1229 }
1230
1231 #[test]
1234 fn test_training_monitor_new() {
1235 let config = make_config();
1236 let monitor = TrainingMonitor::new(&config);
1237 assert!(monitor.metrics_history.is_empty());
1238 assert!(monitor.active_alerts.is_empty());
1239 assert_eq!(monitor.max_history, 10000);
1240 }
1241
1242 #[test]
1243 fn test_training_monitor_update_metrics() {
1244 let config = make_config();
1245 let mut monitor = TrainingMonitor::new(&config);
1246 monitor.update_metrics(make_metrics_simple());
1247 assert_eq!(monitor.metrics_history.len(), 1);
1248 }
1249
1250 #[test]
1251 fn test_training_monitor_history_limit() {
1252 let config = make_config();
1253 let mut monitor = TrainingMonitor::new(&config);
1254 monitor.max_history = 5;
1255 for _ in 0..10 {
1256 monitor.update_metrics(make_metrics_simple());
1257 }
1258 assert_eq!(monitor.metrics_history.len(), 5);
1259 }
1260
1261 #[test]
1262 fn test_training_monitor_get_recent_metrics() {
1263 let config = make_config();
1264 let mut monitor = TrainingMonitor::new(&config);
1265 for _ in 0..5 {
1266 monitor.update_metrics(make_metrics_simple());
1267 }
1268 let recent = monitor.get_recent_metrics(3);
1269 assert_eq!(recent.len(), 3);
1270 }
1271
1272 #[test]
1273 fn test_training_monitor_get_recent_metrics_more_than_available() {
1274 let config = make_config();
1275 let mut monitor = TrainingMonitor::new(&config);
1276 monitor.update_metrics(make_metrics_simple());
1277 let recent = monitor.get_recent_metrics(10);
1278 assert_eq!(recent.len(), 1);
1279 }
1280
1281 #[test]
1282 fn test_training_monitor_set_alert_thresholds() {
1283 let config = make_config();
1284 let mut monitor = TrainingMonitor::new(&config);
1285 let thresholds = AlertThresholds {
1286 loss_increase_threshold: 2.0,
1287 gradient_norm_max: 5.0,
1288 memory_usage_max_mb: 4096.0,
1289 gpu_utilization_min: 0.5,
1290 learning_rate_min: 1e-6,
1291 tokens_per_second_min: 50.0,
1292 };
1293 monitor.set_alert_thresholds(thresholds);
1294 assert!((monitor.alert_thresholds.gradient_norm_max - 5.0).abs() < 1e-9);
1295 }
1296
1297 #[test]
1298 fn test_training_monitor_gradient_explosion_alert() {
1299 let config = make_config();
1300 let mut monitor = TrainingMonitor::new(&config);
1301 let mut metrics = make_metrics_simple();
1302 metrics.gradient_norm = Some(100.0);
1303 monitor.update_metrics(metrics);
1304 assert!(monitor
1305 .active_alerts
1306 .iter()
1307 .any(|a| a.alert_type == AlertType::GradientExplosion));
1308 }
1309
1310 #[test]
1311 fn test_training_monitor_memory_overuse_alert() {
1312 let config = make_config();
1313 let mut monitor = TrainingMonitor::new(&config);
1314 let metrics = make_metrics_with(Some(0.5), Some(0.8), 10000.0);
1315 monitor.update_metrics(metrics);
1316 assert!(monitor.active_alerts.iter().any(|a| a.alert_type == AlertType::MemoryOveruse));
1317 }
1318
1319 #[test]
1320 fn test_training_monitor_low_gpu_alert() {
1321 let config = make_config();
1322 let mut monitor = TrainingMonitor::new(&config);
1323 let mut metrics = make_metrics_simple();
1324 metrics.gpu_utilization = Some(0.1);
1325 monitor.update_metrics(metrics);
1326 assert!(monitor
1327 .active_alerts
1328 .iter()
1329 .any(|a| a.alert_type == AlertType::LowGpuUtilization));
1330 }
1331
1332 #[test]
1333 fn test_training_monitor_slow_token_alert() {
1334 let config = make_config();
1335 let mut monitor = TrainingMonitor::new(&config);
1336 let mut metrics = make_metrics_simple();
1337 metrics.tokens_per_second = Some(10.0);
1338 monitor.update_metrics(metrics);
1339 assert!(monitor
1340 .active_alerts
1341 .iter()
1342 .any(|a| a.alert_type == AlertType::SlowTokenProcessing));
1343 }
1344
1345 #[test]
1346 fn test_training_monitor_no_duplicate_alerts() {
1347 let config = make_config();
1348 let mut monitor = TrainingMonitor::new(&config);
1349 let mut metrics = make_metrics_simple();
1350 metrics.gradient_norm = Some(100.0);
1351 monitor.update_metrics(metrics.clone());
1352 monitor.update_metrics(metrics);
1353 let grad_alerts = monitor
1354 .active_alerts
1355 .iter()
1356 .filter(|a| a.alert_type == AlertType::GradientExplosion)
1357 .count();
1358 assert_eq!(grad_alerts, 1);
1359 }
1360
1361 #[test]
1362 fn test_training_monitor_average_loss_none() {
1363 let config = make_config();
1364 let monitor = TrainingMonitor::new(&config);
1365 assert!(monitor.calculate_average_loss().is_none());
1366 }
1367
1368 #[test]
1369 fn test_training_monitor_average_loss() {
1370 let config = make_config();
1371 let mut monitor = TrainingMonitor::new(&config);
1372 monitor.update_metrics(make_metrics_with(Some(1.0), None, 1024.0));
1373 monitor.update_metrics(make_metrics_with(Some(2.0), None, 1024.0));
1374 let avg = monitor.calculate_average_loss();
1375 assert!(avg.is_some());
1376 assert!((avg.expect("should be some") - 1.5).abs() < 1e-9);
1377 }
1378
1379 #[test]
1380 fn test_training_monitor_best_accuracy_none() {
1381 let config = make_config();
1382 let monitor = TrainingMonitor::new(&config);
1383 assert!(monitor.calculate_best_accuracy().is_none());
1384 }
1385
1386 #[test]
1387 fn test_training_monitor_best_accuracy() {
1388 let config = make_config();
1389 let mut monitor = TrainingMonitor::new(&config);
1390 monitor.update_metrics(make_metrics_with(None, Some(0.7), 1024.0));
1391 monitor.update_metrics(make_metrics_with(None, Some(0.9), 1024.0));
1392 monitor.update_metrics(make_metrics_with(None, Some(0.8), 1024.0));
1393 let best = monitor.calculate_best_accuracy();
1394 assert!(best.is_some());
1395 assert!((best.expect("should be some") - 0.9).abs() < 1e-9);
1396 }
1397
1398 #[test]
1399 fn test_training_monitor_avg_tps_none() {
1400 let config = make_config();
1401 let monitor = TrainingMonitor::new(&config);
1402 assert!(monitor.calculate_average_tokens_per_second().is_none());
1403 }
1404
1405 #[test]
1406 fn test_training_stability_insufficient() {
1407 let config = make_config();
1408 let monitor = TrainingMonitor::new(&config);
1409 assert!(matches!(
1410 monitor.calculate_training_stability(),
1411 TrainingStability::Insufficient
1412 ));
1413 }
1414
1415 #[test]
1416 fn test_convergence_too_early() {
1417 let config = make_config();
1418 let monitor = TrainingMonitor::new(&config);
1419 assert!(matches!(
1420 monitor.assess_convergence(),
1421 ConvergenceStatus::TooEarly
1422 ));
1423 }
1424
1425 #[test]
1426 fn test_generate_training_summary() {
1427 let config = make_config();
1428 let monitor = TrainingMonitor::new(&config);
1429 let summary = monitor.generate_training_summary();
1430 assert_eq!(summary.total_steps, 0);
1431 assert!(matches!(
1432 summary.convergence_status,
1433 ConvergenceStatus::TooEarly
1434 ));
1435 }
1436
1437 #[test]
1440 fn test_model_comparator_new() {
1441 let comparator = ModelComparator::new();
1442 assert!(comparator.models.is_empty());
1443 }
1444
1445 #[test]
1446 fn test_model_comparator_add_model() {
1447 let mut comparator = ModelComparator::new();
1448 comparator.add_model(ModelMetrics {
1449 model_id: "m1".to_string(),
1450 model_name: "Model A".to_string(),
1451 metrics_history: Vec::new(),
1452 final_loss: Some(0.5),
1453 final_accuracy: Some(0.9),
1454 training_time: Duration::from_secs(100),
1455 parameter_count: 1000,
1456 model_size_mb: 10.0,
1457 });
1458 assert_eq!(comparator.models.len(), 1);
1459 }
1460
1461 #[test]
1462 fn test_model_comparator_find_best_model_empty() {
1463 let comparator = ModelComparator::new();
1464 assert!(comparator.find_best_model().is_none());
1465 }
1466
1467 #[test]
1468 fn test_model_comparator_find_best_model() {
1469 let mut comparator = ModelComparator::new();
1470 comparator.add_model(ModelMetrics {
1471 model_id: "m1".to_string(),
1472 model_name: "Model A".to_string(),
1473 metrics_history: Vec::new(),
1474 final_loss: Some(0.5),
1475 final_accuracy: Some(0.9),
1476 training_time: Duration::from_secs(100),
1477 parameter_count: 1000,
1478 model_size_mb: 10.0,
1479 });
1480 comparator.add_model(ModelMetrics {
1481 model_id: "m2".to_string(),
1482 model_name: "Model B".to_string(),
1483 metrics_history: Vec::new(),
1484 final_loss: Some(0.3),
1485 final_accuracy: Some(0.95),
1486 training_time: Duration::from_secs(200),
1487 parameter_count: 2000,
1488 model_size_mb: 20.0,
1489 });
1490 let best = comparator.find_best_model();
1491 assert!(best.is_some());
1492 assert_eq!(best.expect("should find best"), "m2");
1493 }
1494
1495 #[test]
1496 fn test_model_comparator_rank_models() {
1497 let mut comparator = ModelComparator::new();
1498 comparator.add_model(ModelMetrics {
1499 model_id: "m1".to_string(),
1500 model_name: "A".to_string(),
1501 metrics_history: Vec::new(),
1502 final_loss: Some(0.5),
1503 final_accuracy: None,
1504 training_time: Duration::from_secs(100),
1505 parameter_count: 1000,
1506 model_size_mb: 10.0,
1507 });
1508 let ranking = comparator.rank_models();
1509 assert_eq!(ranking.len(), 1);
1510 assert_eq!(ranking[0].rank, 1);
1511 }
1512
1513 #[test]
1514 fn test_model_comparator_generate_recommendation_similar() {
1515 let comparator = ModelComparator::new();
1516 let ma = ModelMetrics {
1517 model_id: "a".to_string(),
1518 model_name: "A".to_string(),
1519 metrics_history: Vec::new(),
1520 final_loss: Some(0.5),
1521 final_accuracy: None,
1522 training_time: Duration::from_secs(100),
1523 parameter_count: 1000,
1524 model_size_mb: 10.0,
1525 };
1526 let rec = comparator.generate_recommendation(&ma, &ma, Some(0.0));
1527 assert!(rec.contains("similarly"));
1528 }
1529
1530 #[test]
1531 fn test_model_comparator_recommendation_reports_missing_metric_not_similarity() {
1532 let comparator = ModelComparator::new();
1535 let ma = ModelMetrics {
1536 model_id: "a".to_string(),
1537 model_name: "A".to_string(),
1538 metrics_history: Vec::new(),
1539 final_loss: None,
1540 final_accuracy: None,
1541 training_time: Duration::from_secs(100),
1542 parameter_count: 1000,
1543 model_size_mb: 10.0,
1544 };
1545 let rec = comparator.generate_recommendation(&ma, &ma, None);
1546 assert!(!rec.contains("similarly"), "got {rec}");
1547 assert!(rec.contains("not recorded"), "got {rec}");
1548 }
1549
1550 #[test]
1551 fn test_performance_difference_is_none_when_metric_never_recorded() {
1552 let comparator = ModelComparator::new();
1553 let unmeasured = ModelMetrics {
1554 model_id: "a".to_string(),
1555 model_name: "A".to_string(),
1556 metrics_history: Vec::new(),
1557 final_loss: None,
1558 final_accuracy: None,
1559 training_time: Duration::from_secs(100),
1560 parameter_count: 1000,
1561 model_size_mb: 10.0,
1562 };
1563 assert_eq!(
1564 comparator.calculate_performance_difference(&unmeasured, &unmeasured),
1565 None,
1566 "no final loss on either side: absence, not a 0.0 tie"
1567 );
1568
1569 let measured_a = model_with_loss_history("a", &[0.5]);
1570 let measured_b = model_with_loss_history("b", &[0.4]);
1571 let diff = comparator
1572 .calculate_performance_difference(&measured_a, &measured_b)
1573 .expect("both models recorded a final loss");
1574 assert!(
1575 (diff - (-0.2)).abs() < 1e-12,
1576 "(0.4 - 0.5)/0.5 = -0.2, got {diff}"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_efficiency_difference_is_none_without_a_reference_scale() {
1582 let comparator = ModelComparator::new();
1583 let zeroed = ModelMetrics {
1584 model_id: "a".to_string(),
1585 model_name: "A".to_string(),
1586 metrics_history: Vec::new(),
1587 final_loss: Some(0.5),
1588 final_accuracy: None,
1589 training_time: Duration::ZERO,
1590 parameter_count: 1000,
1591 model_size_mb: 0.0,
1592 };
1593 assert_eq!(
1594 comparator.calculate_efficiency_difference(&zeroed, &zeroed),
1595 None,
1596 "dividing by a zero reference used to yield NaN/inf, not a real ratio"
1597 );
1598
1599 let a = model_with_loss_history("a", &[0.5]);
1600 let mut b = model_with_loss_history("b", &[0.5]);
1601 b.training_time = Duration::from_secs(150);
1602 b.model_size_mb = 20.0;
1603 let diff = comparator
1604 .calculate_efficiency_difference(&a, &b)
1605 .expect("both scales are non-zero");
1606 assert!((diff - 0.75).abs() < 1e-12, "got {diff}");
1608 }
1609
1610 #[test]
1611 fn test_clear_alert_removes_only_the_requested_alert_type() {
1612 let config = DebugConfig::default();
1613 let mut monitor = TrainingMonitor::new(&config);
1614 for alert_type in [
1615 AlertType::LossIncrease,
1616 AlertType::MemoryOveruse,
1617 AlertType::TrainingStalled,
1618 ] {
1619 monitor.active_alerts.push(TrainingAlert {
1620 alert_type,
1621 severity: AlertSeverity::Warning,
1622 message: "test".to_string(),
1623 timestamp: SystemTime::now(),
1624 metric_value: 1.0,
1625 threshold: 0.5,
1626 suggested_action: "none".to_string(),
1627 });
1628 }
1629 assert_eq!(monitor.get_active_alerts().len(), 3);
1630
1631 monitor.clear_alert(AlertType::MemoryOveruse);
1632
1633 let remaining: Vec<&AlertType> =
1635 monitor.get_active_alerts().iter().map(|a| &a.alert_type).collect();
1636 assert_eq!(remaining.len(), 2, "only the MemoryOveruse alert may go");
1637 assert!(remaining.contains(&&AlertType::LossIncrease));
1638 assert!(remaining.contains(&&AlertType::TrainingStalled));
1639 assert!(!remaining.contains(&&AlertType::MemoryOveruse));
1640
1641 monitor.clear_alert(AlertType::GradientExplosion);
1643 assert_eq!(monitor.get_active_alerts().len(), 2);
1644 }
1645
1646 fn model_with_loss_history(model_id: &str, losses: &[f64]) -> ModelMetrics {
1647 ModelMetrics {
1648 model_id: model_id.to_string(),
1649 model_name: model_id.to_string(),
1650 metrics_history: losses
1651 .iter()
1652 .map(|&l| make_metrics_with(Some(l), None, 1024.0))
1653 .collect(),
1654 final_loss: losses.last().copied(),
1655 final_accuracy: None,
1656 training_time: Duration::from_secs(100),
1657 parameter_count: 1000,
1658 model_size_mb: 10.0,
1659 }
1660 }
1661
1662 #[test]
1663 fn test_statistical_significance_none_with_empty_history() {
1664 let comparator = ModelComparator::new();
1667 let ma = model_with_loss_history("a", &[]);
1668 let mb = model_with_loss_history("b", &[]);
1669 assert_eq!(comparator.test_statistical_significance(&ma, &mb), None);
1670 }
1671
1672 #[test]
1673 fn test_statistical_significance_none_with_single_sample() {
1674 let comparator = ModelComparator::new();
1677 let ma = model_with_loss_history("a", &[0.5]);
1678 let mb = model_with_loss_history("b", &[0.2]);
1679 assert_eq!(comparator.test_statistical_significance(&ma, &mb), None);
1680 }
1681
1682 #[test]
1683 fn test_statistical_significance_true_for_clearly_separated_models() {
1684 let comparator = ModelComparator::new();
1687 let ma = model_with_loss_history("a", &[0.50, 0.51, 0.49, 0.50, 0.52, 0.48, 0.50, 0.51]);
1688 let mb = model_with_loss_history("b", &[0.20, 0.21, 0.19, 0.20, 0.22, 0.18, 0.20, 0.21]);
1689 assert_eq!(
1690 comparator.test_statistical_significance(&ma, &mb),
1691 Some(true)
1692 );
1693 }
1694
1695 #[test]
1696 fn test_statistical_significance_false_for_overlapping_models() {
1697 let comparator = ModelComparator::new();
1702 let ma = model_with_loss_history("a", &[0.50, 0.55, 0.45, 0.52, 0.48, 0.51, 0.49, 0.53]);
1703 let mb = model_with_loss_history("b", &[0.51, 0.46, 0.54, 0.49, 0.52, 0.47, 0.53, 0.50]);
1704 assert_eq!(
1705 comparator.test_statistical_significance(&ma, &mb),
1706 Some(false)
1707 );
1708 }
1709
1710 #[test]
1711 fn test_compare_two_models_publishes_option_significance() {
1712 let comparator = ModelComparator::new();
1716 let ma = model_with_loss_history("a", &[0.50, 0.51, 0.49, 0.50, 0.52, 0.48, 0.50, 0.51]);
1717 let mb = model_with_loss_history("b", &[0.20, 0.21, 0.19, 0.20, 0.22, 0.18, 0.20, 0.21]);
1718 let comparison = comparator.compare_two_models(&ma, &mb);
1719 assert_eq!(comparison.statistical_significance, Some(true));
1720 }
1721
1722 #[test]
1725 fn test_hyperparameter_explorer_new() {
1726 let explorer = HyperparameterExplorer::new();
1727 assert!(explorer.experiments.is_empty());
1728 }
1729
1730 #[test]
1731 fn test_hyperparameter_explorer_add_experiment() {
1732 let mut explorer = HyperparameterExplorer::new();
1733 explorer.add_experiment(HyperparameterExperiment {
1734 experiment_id: "exp1".to_string(),
1735 hyperparameters: HashMap::new(),
1736 results: ExperimentResults {
1737 final_loss: Some(0.5),
1738 final_accuracy: Some(0.9),
1739 training_time: Duration::from_secs(100),
1740 convergence_epoch: Some(50),
1741 best_validation_score: Some(0.88),
1742 },
1743 status: ExperimentStatus::Completed,
1744 });
1745 assert_eq!(explorer.experiments.len(), 1);
1746 }
1747
1748 #[test]
1749 fn test_hyperparameter_explorer_get_recommendations() {
1750 let explorer = HyperparameterExplorer::new();
1751 let recs = explorer.get_recommendations();
1752 assert_eq!(recs.total_experiments, 0);
1753 assert!(!recs.parameter_importance.is_empty());
1754 }
1755
1756 #[test]
1757 fn test_hyperparameter_explorer_suggest_next_experiments() {
1758 let explorer = HyperparameterExplorer::new();
1759 let suggestions = explorer.suggest_next_experiments(3);
1760 assert_eq!(suggestions.len(), 3);
1761 }
1762
1763 #[test]
1766 fn test_interactive_dashboard_new() {
1767 let config = make_config();
1768 let dashboard = InteractiveDashboard::new(&config);
1769 assert!(dashboard.websocket_server.is_none());
1770 }
1771
1772 #[test]
1773 fn test_interactive_dashboard_update() {
1774 let config = make_config();
1775 let mut dashboard = InteractiveDashboard::new(&config);
1776 dashboard.update(make_metrics_simple());
1777 assert_eq!(dashboard.training_monitor.metrics_history.len(), 1);
1778 }
1779
1780 #[test]
1781 fn test_interactive_dashboard_snapshot() {
1782 let config = make_config();
1783 let dashboard = InteractiveDashboard::new(&config);
1784 let snapshot = dashboard.get_dashboard_snapshot();
1785 assert!(snapshot.recent_metrics.is_empty());
1786 }
1787
1788 #[test]
1789 fn test_interactive_dashboard_generate_recommendations() {
1790 let config = make_config();
1791 let dashboard = InteractiveDashboard::new(&config);
1792 let recs = dashboard.generate_recommendations();
1793 assert!(!recs.is_empty());
1794 }
1795
1796 #[test]
1797 fn test_interactive_dashboard_generate_key_insights() {
1798 let config = make_config();
1799 let dashboard = InteractiveDashboard::new(&config);
1800 let insights = dashboard.generate_key_insights();
1801 assert!(insights.is_empty() || !insights.is_empty());
1803 }
1804
1805 #[test]
1808 fn test_comparison_config_default() {
1809 let config = ComparisonConfig::default();
1810 assert_eq!(config.primary_metric, "loss");
1811 assert_eq!(config.comparison_window, 100);
1812 }
1813
1814 #[test]
1817 fn test_search_space_default() {
1818 let space = HyperparameterSearchSpace::default();
1819 assert!(space.learning_rate.0 < space.learning_rate.1);
1820 assert!(space.batch_size.0 < space.batch_size.1);
1821 }
1822}