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) {
136 self.active_alerts.retain(|alert| !matches!(&alert.alert_type, _alert_type));
137 }
138
139 pub fn set_alert_thresholds(&mut self, thresholds: AlertThresholds) {
141 self.alert_thresholds = thresholds;
142 }
143
144 pub fn generate_training_summary(&self) -> TrainingSummary {
146 let total_duration = self.start_time.elapsed();
147 let total_steps = self.metrics_history.len();
148
149 let avg_loss = self.calculate_average_loss();
150 let best_accuracy = self.calculate_best_accuracy();
151 let avg_tokens_per_second = self.calculate_average_tokens_per_second();
152 let training_stability = self.calculate_training_stability();
153
154 TrainingSummary {
155 total_duration,
156 total_steps,
157 avg_loss,
158 best_accuracy,
159 avg_tokens_per_second,
160 training_stability,
161 active_alerts_count: self.active_alerts.len(),
162 convergence_status: self.assess_convergence(),
163 }
164 }
165
166 fn check_alerts(&mut self, metrics: &DashboardMetrics) {
167 if let Some(current_loss) = metrics.loss {
169 if let Some(prev_metrics) =
170 self.metrics_history.get(self.metrics_history.len().saturating_sub(10))
171 {
172 if let Some(prev_loss) = prev_metrics.loss {
173 if current_loss > prev_loss * self.alert_thresholds.loss_increase_threshold {
174 self.add_alert(TrainingAlert {
175 alert_type: AlertType::LossIncrease,
176 severity: AlertSeverity::Warning,
177 message: "Loss has increased significantly".to_string(),
178 timestamp: SystemTime::now(),
179 metric_value: current_loss,
180 threshold: prev_loss * self.alert_thresholds.loss_increase_threshold,
181 suggested_action: "Check learning rate or data quality".to_string(),
182 });
183 }
184 }
185 }
186 }
187
188 if let Some(grad_norm) = metrics.gradient_norm {
190 if grad_norm > self.alert_thresholds.gradient_norm_max {
191 self.add_alert(TrainingAlert {
192 alert_type: AlertType::GradientExplosion,
193 severity: AlertSeverity::Critical,
194 message: "Gradient explosion detected".to_string(),
195 timestamp: SystemTime::now(),
196 metric_value: grad_norm,
197 threshold: self.alert_thresholds.gradient_norm_max,
198 suggested_action: "Apply gradient clipping or reduce learning rate".to_string(),
199 });
200 }
201 }
202
203 if metrics.memory_usage_mb > self.alert_thresholds.memory_usage_max_mb {
205 self.add_alert(TrainingAlert {
206 alert_type: AlertType::MemoryOveruse,
207 severity: AlertSeverity::Warning,
208 message: "High memory usage detected".to_string(),
209 timestamp: SystemTime::now(),
210 metric_value: metrics.memory_usage_mb,
211 threshold: self.alert_thresholds.memory_usage_max_mb,
212 suggested_action: "Reduce batch size or enable gradient checkpointing".to_string(),
213 });
214 }
215
216 if let Some(gpu_util) = metrics.gpu_utilization {
218 if gpu_util < self.alert_thresholds.gpu_utilization_min {
219 self.add_alert(TrainingAlert {
220 alert_type: AlertType::LowGpuUtilization,
221 severity: AlertSeverity::Info,
222 message: "Low GPU utilization".to_string(),
223 timestamp: SystemTime::now(),
224 metric_value: gpu_util,
225 threshold: self.alert_thresholds.gpu_utilization_min,
226 suggested_action: "Increase batch size or check data loading".to_string(),
227 });
228 }
229 }
230
231 if let Some(tps) = metrics.tokens_per_second {
233 if tps < self.alert_thresholds.tokens_per_second_min {
234 self.add_alert(TrainingAlert {
235 alert_type: AlertType::SlowTokenProcessing,
236 severity: AlertSeverity::Warning,
237 message: "Slow token processing detected".to_string(),
238 timestamp: SystemTime::now(),
239 metric_value: tps,
240 threshold: self.alert_thresholds.tokens_per_second_min,
241 suggested_action: "Optimize model or increase batch size".to_string(),
242 });
243 }
244 }
245 }
246
247 fn add_alert(&mut self, alert: TrainingAlert) {
248 if !self.active_alerts.iter().any(|a| a.alert_type == alert.alert_type) {
250 self.active_alerts.push(alert);
251 }
252 }
253
254 fn calculate_average_loss(&self) -> Option<f64> {
255 let losses: Vec<f64> = self.metrics_history.iter().filter_map(|m| m.loss).collect();
256
257 if losses.is_empty() {
258 None
259 } else {
260 Some(losses.iter().sum::<f64>() / losses.len() as f64)
261 }
262 }
263
264 fn calculate_best_accuracy(&self) -> Option<f64> {
265 self.metrics_history
266 .iter()
267 .filter_map(|m| m.accuracy)
268 .fold(None, |acc, x| match acc {
269 None => Some(x),
270 Some(y) => Some(x.max(y)),
271 })
272 }
273
274 fn calculate_average_tokens_per_second(&self) -> Option<f64> {
275 let tps_values: Vec<f64> =
276 self.metrics_history.iter().filter_map(|m| m.tokens_per_second).collect();
277
278 if tps_values.is_empty() {
279 None
280 } else {
281 Some(tps_values.iter().sum::<f64>() / tps_values.len() as f64)
282 }
283 }
284
285 fn calculate_training_stability(&self) -> TrainingStability {
286 if self.metrics_history.len() < 10 {
287 return TrainingStability::Insufficient;
288 }
289
290 let recent_losses: Vec<f64> =
291 self.metrics_history.iter().rev().take(50).filter_map(|m| m.loss).collect();
292
293 if recent_losses.len() < 10 {
294 return TrainingStability::Insufficient;
295 }
296
297 let mean_loss = recent_losses.iter().sum::<f64>() / recent_losses.len() as f64;
299 let variance = recent_losses.iter().map(|&x| (x - mean_loss).powi(2)).sum::<f64>()
300 / recent_losses.len() as f64;
301
302 let std_dev = variance.sqrt();
303 let coefficient_of_variation = if mean_loss != 0.0 { std_dev / mean_loss } else { 0.0 };
304
305 match coefficient_of_variation {
306 cv if cv < 0.1 => TrainingStability::Stable,
307 cv if cv < 0.3 => TrainingStability::Moderate,
308 _ => TrainingStability::Unstable,
309 }
310 }
311
312 fn assess_convergence(&self) -> ConvergenceStatus {
313 if self.metrics_history.len() < 50 {
314 return ConvergenceStatus::TooEarly;
315 }
316
317 let recent_losses: Vec<f64> =
318 self.metrics_history.iter().rev().take(100).filter_map(|m| m.loss).collect();
319
320 if recent_losses.len() < 50 {
321 return ConvergenceStatus::TooEarly;
322 }
323
324 let first_half_avg =
326 recent_losses[25..].iter().sum::<f64>() / (recent_losses.len() - 25) as f64;
327 let second_half_avg = recent_losses[..25].iter().sum::<f64>() / 25.0;
328
329 if second_half_avg < first_half_avg * 0.95 {
330 ConvergenceStatus::Converging
331 } else if (second_half_avg - first_half_avg).abs() / first_half_avg < 0.01 {
332 ConvergenceStatus::Converged
333 } else {
334 ConvergenceStatus::Diverging
335 }
336 }
337}
338
339#[derive(Debug)]
341pub struct ModelComparator {
342 models: HashMap<String, ModelMetrics>,
343 comparison_config: ComparisonConfig,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ModelMetrics {
348 pub model_id: String,
349 pub model_name: String,
350 pub metrics_history: Vec<DashboardMetrics>,
351 pub final_loss: Option<f64>,
352 pub final_accuracy: Option<f64>,
353 pub training_time: Duration,
354 pub parameter_count: usize,
355 pub model_size_mb: f64,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ComparisonConfig {
360 pub primary_metric: String,
361 pub comparison_window: usize,
362 pub significance_threshold: f64,
363}
364
365impl Default for ComparisonConfig {
366 fn default() -> Self {
367 Self {
368 primary_metric: "loss".to_string(),
369 comparison_window: 100,
370 significance_threshold: 0.05,
371 }
372 }
373}
374
375impl ModelComparator {
376 pub fn new() -> Self {
378 Self {
379 models: HashMap::new(),
380 comparison_config: ComparisonConfig::default(),
381 }
382 }
383
384 pub fn add_model(&mut self, model_metrics: ModelMetrics) {
386 self.models.insert(model_metrics.model_id.clone(), model_metrics);
387 }
388
389 pub fn compare_models(&self) -> ModelComparisonReport {
391 let mut comparisons = Vec::new();
392 let model_ids: Vec<String> = self.models.keys().cloned().collect();
393
394 for i in 0..model_ids.len() {
395 for j in (i + 1)..model_ids.len() {
396 let model_a = &self.models[&model_ids[i]];
397 let model_b = &self.models[&model_ids[j]];
398
399 let comparison = self.compare_two_models(model_a, model_b);
400 comparisons.push(comparison);
401 }
402 }
403
404 let best_model = self.find_best_model();
405 let ranking = self.rank_models();
406
407 ModelComparisonReport {
408 comparisons,
409 best_model,
410 ranking,
411 comparison_config: self.comparison_config.clone(),
412 }
413 }
414
415 fn compare_two_models(
416 &self,
417 model_a: &ModelMetrics,
418 model_b: &ModelMetrics,
419 ) -> ModelComparison {
420 let performance_diff = self.calculate_performance_difference(model_a, model_b);
421 let efficiency_diff = self.calculate_efficiency_difference(model_a, model_b);
422 let statistical_significance = self.test_statistical_significance(model_a, model_b);
423
424 ModelComparison {
425 model_a_id: model_a.model_id.clone(),
426 model_b_id: model_b.model_id.clone(),
427 performance_difference: performance_diff,
428 efficiency_difference: efficiency_diff,
429 statistical_significance,
430 recommendation: self.generate_recommendation(model_a, model_b, performance_diff),
431 }
432 }
433
434 fn calculate_performance_difference(
435 &self,
436 model_a: &ModelMetrics,
437 model_b: &ModelMetrics,
438 ) -> f64 {
439 match self.comparison_config.primary_metric.as_str() {
440 "loss" => {
441 if let (Some(loss_a), Some(loss_b)) = (model_a.final_loss, model_b.final_loss) {
442 (loss_b - loss_a) / loss_a } else {
444 0.0
445 }
446 },
447 "accuracy" => {
448 if let (Some(acc_a), Some(acc_b)) = (model_a.final_accuracy, model_b.final_accuracy)
449 {
450 (acc_b - acc_a) / acc_a } else {
452 0.0
453 }
454 },
455 _ => 0.0,
456 }
457 }
458
459 fn calculate_efficiency_difference(
460 &self,
461 model_a: &ModelMetrics,
462 model_b: &ModelMetrics,
463 ) -> f64 {
464 let time_diff =
466 model_b.training_time.as_secs_f64() / model_a.training_time.as_secs_f64() - 1.0;
467
468 let size_diff = model_b.model_size_mb / model_a.model_size_mb - 1.0;
470
471 (time_diff + size_diff) / 2.0
473 }
474
475 fn test_statistical_significance(
476 &self,
477 _model_a: &ModelMetrics,
478 _model_b: &ModelMetrics,
479 ) -> bool {
480 true }
483
484 fn generate_recommendation(
485 &self,
486 model_a: &ModelMetrics,
487 model_b: &ModelMetrics,
488 perf_diff: f64,
489 ) -> String {
490 if perf_diff.abs() < 0.01 {
491 "Models perform similarly - choose based on other factors".to_string()
492 } else if perf_diff < 0.0 {
493 format!(
494 "Model {} performs {:.1}% better",
495 model_a.model_name,
496 perf_diff.abs() * 100.0
497 )
498 } else {
499 format!(
500 "Model {} performs {:.1}% better",
501 model_b.model_name,
502 perf_diff * 100.0
503 )
504 }
505 }
506
507 fn find_best_model(&self) -> Option<String> {
508 let mut best_model = None;
509 let mut best_score = f64::NEG_INFINITY;
510
511 for model in self.models.values() {
512 let score = match self.comparison_config.primary_metric.as_str() {
513 "loss" => model.final_loss.map(|l| -l).unwrap_or(f64::NEG_INFINITY),
514 "accuracy" => model.final_accuracy.unwrap_or(0.0),
515 _ => 0.0,
516 };
517
518 if score > best_score {
519 best_score = score;
520 best_model = Some(model.model_id.clone());
521 }
522 }
523
524 best_model
525 }
526
527 fn rank_models(&self) -> Vec<ModelRanking> {
528 let mut rankings: Vec<ModelRanking> = self
529 .models
530 .values()
531 .map(|model| {
532 let score = match self.comparison_config.primary_metric.as_str() {
533 "loss" => model.final_loss.map(|l| -l).unwrap_or(f64::NEG_INFINITY),
534 "accuracy" => model.final_accuracy.unwrap_or(0.0),
535 _ => 0.0,
536 };
537
538 ModelRanking {
539 model_id: model.model_id.clone(),
540 model_name: model.model_name.clone(),
541 score,
542 rank: 0, }
544 })
545 .collect();
546
547 rankings.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
548
549 for (i, ranking) in rankings.iter_mut().enumerate() {
550 ranking.rank = i + 1;
551 }
552
553 rankings
554 }
555}
556
557#[derive(Debug)]
559pub struct HyperparameterExplorer {
560 experiments: HashMap<String, HyperparameterExperiment>,
561 search_space: HyperparameterSearchSpace,
562 optimization_history: Vec<OptimizationStep>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct HyperparameterExperiment {
567 pub experiment_id: String,
568 pub hyperparameters: HashMap<String, HyperparameterValue>,
569 pub results: ExperimentResults,
570 pub status: ExperimentStatus,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub enum HyperparameterValue {
575 Float(f64),
576 Integer(i64),
577 String(String),
578 Boolean(bool),
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct ExperimentResults {
583 pub final_loss: Option<f64>,
584 pub final_accuracy: Option<f64>,
585 pub training_time: Duration,
586 pub convergence_epoch: Option<u32>,
587 pub best_validation_score: Option<f64>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub enum ExperimentStatus {
592 Running,
593 Completed,
594 Failed,
595 Cancelled,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct HyperparameterSearchSpace {
600 pub learning_rate: (f64, f64),
601 pub batch_size: (i64, i64),
602 pub dropout_rate: (f64, f64),
603 pub weight_decay: (f64, f64),
604 pub num_layers: (i64, i64),
605 pub hidden_size: (i64, i64),
606}
607
608impl Default for HyperparameterSearchSpace {
609 fn default() -> Self {
610 Self {
611 learning_rate: (1e-5, 1e-1),
612 batch_size: (4, 128),
613 dropout_rate: (0.0, 0.5),
614 weight_decay: (0.0, 1e-2),
615 num_layers: (1, 12),
616 hidden_size: (64, 2048),
617 }
618 }
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct OptimizationStep {
623 pub step: usize,
624 pub best_experiment_id: String,
625 pub best_score: f64,
626 pub exploration_count: usize,
627 pub exploitation_count: usize,
628}
629
630impl HyperparameterExplorer {
631 pub fn new() -> Self {
633 Self {
634 experiments: HashMap::new(),
635 search_space: HyperparameterSearchSpace::default(),
636 optimization_history: Vec::new(),
637 }
638 }
639
640 pub fn add_experiment(&mut self, experiment: HyperparameterExperiment) {
642 self.experiments.insert(experiment.experiment_id.clone(), experiment);
643 }
644
645 pub fn get_recommendations(&self) -> HyperparameterRecommendations {
647 let best_experiments = self.find_best_experiments(5);
648 let parameter_importance = self.analyze_parameter_importance();
649 let suggested_ranges = self.suggest_search_ranges();
650 let next_experiments = self.suggest_next_experiments(3);
651
652 HyperparameterRecommendations {
653 best_experiments,
654 parameter_importance,
655 suggested_ranges,
656 next_experiments,
657 total_experiments: self.experiments.len(),
658 }
659 }
660
661 fn find_best_experiments(&self, limit: usize) -> Vec<String> {
662 let mut experiments: Vec<_> = self.experiments.values().collect();
663 experiments.sort_by(|a, b| {
664 let score_a = a.results.final_loss.unwrap_or(f64::INFINITY);
665 let score_b = b.results.final_loss.unwrap_or(f64::INFINITY);
666 score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal)
667 });
668
669 experiments.iter().take(limit).map(|exp| exp.experiment_id.clone()).collect()
670 }
671
672 fn analyze_parameter_importance(&self) -> HashMap<String, f64> {
673 let mut importance = HashMap::new();
675 importance.insert("learning_rate".to_string(), 0.8);
676 importance.insert("batch_size".to_string(), 0.6);
677 importance.insert("dropout_rate".to_string(), 0.4);
678 importance.insert("weight_decay".to_string(), 0.3);
679 importance
680 }
681
682 fn suggest_search_ranges(&self) -> HashMap<String, (f64, f64)> {
683 let mut ranges = HashMap::new();
685 ranges.insert("learning_rate".to_string(), (1e-4, 1e-2));
686 ranges.insert("dropout_rate".to_string(), (0.1, 0.3));
687 ranges
688 }
689
690 fn suggest_next_experiments(&self, count: usize) -> Vec<HashMap<String, HyperparameterValue>> {
691 let mut suggestions = Vec::new();
692
693 for i in 0..count {
694 let mut params = HashMap::new();
695
696 params.insert(
698 "learning_rate".to_string(),
699 HyperparameterValue::Float(0.001 * (1.0 + i as f64 * 0.5)),
700 );
701 params.insert(
702 "batch_size".to_string(),
703 HyperparameterValue::Integer(32 * (1 + i as i64)),
704 );
705 params.insert(
706 "dropout_rate".to_string(),
707 HyperparameterValue::Float(0.1 + i as f64 * 0.1),
708 );
709
710 suggestions.push(params);
711 }
712
713 suggestions
714 }
715}
716
717#[derive(Debug)]
719pub struct InteractiveDashboard {
720 config: DebugConfig,
721 training_monitor: TrainingMonitor,
722 model_comparator: ModelComparator,
723 hyperparameter_explorer: HyperparameterExplorer,
724 dashboard_state: DashboardState,
725 websocket_server: Option<WebSocketServer>,
726}
727
728#[derive(Debug, Serialize, Deserialize)]
729pub struct DashboardState {
730 pub active_session_id: Option<Uuid>,
731 pub refresh_rate_ms: u64,
732 pub auto_alerts: bool,
733 pub display_mode: DisplayMode,
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
737pub enum DisplayMode {
738 Overview,
739 DetailedMetrics,
740 ModelComparison,
741 HyperparameterOptimization,
742 AlertsOnly,
743}
744
745#[derive(Debug)]
747pub struct WebSocketServer {
748 port: u16,
749 connected_clients: Arc<Mutex<Vec<String>>>,
750}
751
752impl InteractiveDashboard {
753 pub fn new(config: &DebugConfig) -> Self {
755 Self {
756 config: config.clone(),
757 training_monitor: TrainingMonitor::new(config),
758 model_comparator: ModelComparator::new(),
759 hyperparameter_explorer: HyperparameterExplorer::new(),
760 dashboard_state: DashboardState {
761 active_session_id: None,
762 refresh_rate_ms: 1000,
763 auto_alerts: true,
764 display_mode: DisplayMode::Overview,
765 },
766 websocket_server: None,
767 }
768 }
769
770 pub async fn start(&mut self, port: Option<u16>) -> Result<()> {
772 let port = port.unwrap_or(8080);
773
774 self.websocket_server = Some(WebSocketServer {
775 port,
776 connected_clients: Arc::new(Mutex::new(Vec::new())),
777 });
778
779 tracing::info!("Interactive dashboard started on port {}", port);
780 Ok(())
781 }
782
783 pub fn update(&mut self, metrics: DashboardMetrics) {
785 self.training_monitor.update_metrics(metrics.clone());
786
787 if let Some(_ws_server) = &self.websocket_server {
789 self.broadcast_update(metrics);
790 }
791 }
792
793 pub fn get_dashboard_snapshot(&self) -> DashboardSnapshot {
795 let training_summary = self.training_monitor.generate_training_summary();
796 let recent_metrics = self.training_monitor.get_recent_metrics(100);
797 let active_alerts = self.training_monitor.get_active_alerts().to_vec();
798 let model_comparison = self.model_comparator.compare_models();
799 let hyperparameter_recommendations = self.hyperparameter_explorer.get_recommendations();
800
801 DashboardSnapshot {
802 timestamp: SystemTime::now(),
803 training_summary,
804 recent_metrics,
805 active_alerts,
806 model_comparison,
807 hyperparameter_recommendations,
808 dashboard_state: DashboardState {
809 active_session_id: self.dashboard_state.active_session_id,
810 refresh_rate_ms: self.dashboard_state.refresh_rate_ms,
811 auto_alerts: self.dashboard_state.auto_alerts,
812 display_mode: self.dashboard_state.display_mode.clone(),
813 },
814 }
815 }
816
817 pub async fn export_dashboard_data(&self, path: &str) -> Result<()> {
819 let snapshot = self.get_dashboard_snapshot();
820 let json = serde_json::to_string_pretty(&snapshot)?;
821 tokio::fs::write(path, json).await?;
822 Ok(())
823 }
824
825 fn broadcast_update(&self, _metrics: DashboardMetrics) {
826 tracing::debug!("Broadcasting dashboard update to connected clients");
828 }
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize)]
834pub struct TrainingSummary {
835 pub total_duration: Duration,
836 pub total_steps: usize,
837 pub avg_loss: Option<f64>,
838 pub best_accuracy: Option<f64>,
839 pub avg_tokens_per_second: Option<f64>,
840 pub training_stability: TrainingStability,
841 pub active_alerts_count: usize,
842 pub convergence_status: ConvergenceStatus,
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize)]
846pub enum TrainingStability {
847 Stable,
848 Moderate,
849 Unstable,
850 Insufficient,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize)]
854pub enum ConvergenceStatus {
855 TooEarly,
856 Converging,
857 Converged,
858 Diverging,
859}
860
861#[derive(Debug, Serialize, Deserialize)]
862pub struct ModelComparisonReport {
863 pub comparisons: Vec<ModelComparison>,
864 pub best_model: Option<String>,
865 pub ranking: Vec<ModelRanking>,
866 pub comparison_config: ComparisonConfig,
867}
868
869#[derive(Debug, Serialize, Deserialize)]
870pub struct ModelComparison {
871 pub model_a_id: String,
872 pub model_b_id: String,
873 pub performance_difference: f64,
874 pub efficiency_difference: f64,
875 pub statistical_significance: bool,
876 pub recommendation: String,
877}
878
879#[derive(Debug, Serialize, Deserialize)]
880pub struct ModelRanking {
881 pub model_id: String,
882 pub model_name: String,
883 pub score: f64,
884 pub rank: usize,
885}
886
887#[derive(Debug, Serialize, Deserialize)]
888pub struct HyperparameterRecommendations {
889 pub best_experiments: Vec<String>,
890 pub parameter_importance: HashMap<String, f64>,
891 pub suggested_ranges: HashMap<String, (f64, f64)>,
892 pub next_experiments: Vec<HashMap<String, HyperparameterValue>>,
893 pub total_experiments: usize,
894}
895
896#[derive(Debug, Serialize, Deserialize)]
897pub struct DashboardSnapshot {
898 pub timestamp: SystemTime,
899 pub training_summary: TrainingSummary,
900 pub recent_metrics: Vec<DashboardMetrics>,
901 pub active_alerts: Vec<TrainingAlert>,
902 pub model_comparison: ModelComparisonReport,
903 pub hyperparameter_recommendations: HyperparameterRecommendations,
904 pub dashboard_state: DashboardState,
905}
906
907#[derive(Debug, Serialize, Deserialize)]
909pub struct DashboardReport {
910 pub session_duration: Duration,
911 pub total_metrics_recorded: usize,
912 pub alerts_triggered: usize,
913 pub models_compared: usize,
914 pub experiments_tracked: usize,
915 pub performance_summary: TrainingSummary,
916 pub key_insights: Vec<String>,
917 pub recommendations: Vec<String>,
918}
919
920impl InteractiveDashboard {
921 pub async fn generate_report(&self) -> Result<DashboardReport> {
923 let training_summary = self.training_monitor.generate_training_summary();
924 let total_metrics = self.training_monitor.metrics_history.len();
925 let alerts_count = self.training_monitor.active_alerts.len();
926 let models_count = self.model_comparator.models.len();
927 let experiments_count = self.hyperparameter_explorer.experiments.len();
928
929 let key_insights = self.generate_key_insights();
930 let recommendations = self.generate_recommendations();
931
932 Ok(DashboardReport {
933 session_duration: training_summary.total_duration,
934 total_metrics_recorded: total_metrics,
935 alerts_triggered: alerts_count,
936 models_compared: models_count,
937 experiments_tracked: experiments_count,
938 performance_summary: training_summary,
939 key_insights,
940 recommendations,
941 })
942 }
943
944 fn generate_key_insights(&self) -> Vec<String> {
945 let mut insights = Vec::new();
946
947 match self.training_monitor.generate_training_summary().training_stability {
949 TrainingStability::Stable => insights.push("Training is proceeding stably".to_string()),
950 TrainingStability::Unstable => insights.push(
951 "Training shows high variance - consider adjusting hyperparameters".to_string(),
952 ),
953 _ => {},
954 }
955
956 if self.model_comparator.models.len() > 1 {
958 let comparison = self.model_comparator.compare_models();
959 if let Some(best_model) = comparison.best_model {
960 insights.push(format!("Best performing model: {}", best_model));
961 }
962 }
963
964 let critical_alerts = self
966 .training_monitor
967 .active_alerts
968 .iter()
969 .filter(|alert| matches!(alert.severity, AlertSeverity::Critical))
970 .count();
971
972 if critical_alerts > 0 {
973 insights.push(format!(
974 "{} critical alerts require immediate attention",
975 critical_alerts
976 ));
977 }
978
979 insights
980 }
981
982 fn generate_recommendations(&self) -> Vec<String> {
983 let mut recommendations = Vec::new();
984
985 for alert in &self.training_monitor.active_alerts {
987 if matches!(alert.severity, AlertSeverity::Critical) {
988 recommendations.push(alert.suggested_action.clone());
989 }
990 }
991
992 if self.hyperparameter_explorer.experiments.len() > 5 {
994 recommendations.push(
995 "Continue hyperparameter optimization with narrowed search ranges".to_string(),
996 );
997 }
998
999 if self.model_comparator.models.len() > 1 {
1001 recommendations
1002 .push("Focus on the best performing model architecture for production".to_string());
1003 }
1004
1005 if recommendations.is_empty() {
1006 recommendations.push("Continue monitoring training progress".to_string());
1007 }
1008
1009 recommendations
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 fn make_config() -> DebugConfig {
1018 DebugConfig::default()
1019 }
1020
1021 fn make_metrics_with(
1022 loss: Option<f64>,
1023 accuracy: Option<f64>,
1024 memory_mb: f64,
1025 ) -> DashboardMetrics {
1026 DashboardMetrics {
1027 timestamp: SystemTime::now(),
1028 loss,
1029 accuracy,
1030 learning_rate: Some(0.001),
1031 memory_usage_mb: memory_mb,
1032 gpu_utilization: Some(0.8),
1033 tokens_per_second: Some(200.0),
1034 gradient_norm: Some(1.0),
1035 epoch: Some(1),
1036 step: Some(100),
1037 }
1038 }
1039
1040 fn make_metrics_simple() -> DashboardMetrics {
1041 make_metrics_with(Some(0.5), Some(0.85), 2048.0)
1042 }
1043
1044 #[test]
1047 fn test_alert_thresholds_default() {
1048 let thresholds = AlertThresholds::default();
1049 assert!((thresholds.loss_increase_threshold - 1.5).abs() < 1e-9);
1050 assert!((thresholds.gradient_norm_max - 10.0).abs() < 1e-9);
1051 assert!((thresholds.memory_usage_max_mb - 8192.0).abs() < 1e-9);
1052 }
1053
1054 #[test]
1057 fn test_training_monitor_new() {
1058 let config = make_config();
1059 let monitor = TrainingMonitor::new(&config);
1060 assert!(monitor.metrics_history.is_empty());
1061 assert!(monitor.active_alerts.is_empty());
1062 assert_eq!(monitor.max_history, 10000);
1063 }
1064
1065 #[test]
1066 fn test_training_monitor_update_metrics() {
1067 let config = make_config();
1068 let mut monitor = TrainingMonitor::new(&config);
1069 monitor.update_metrics(make_metrics_simple());
1070 assert_eq!(monitor.metrics_history.len(), 1);
1071 }
1072
1073 #[test]
1074 fn test_training_monitor_history_limit() {
1075 let config = make_config();
1076 let mut monitor = TrainingMonitor::new(&config);
1077 monitor.max_history = 5;
1078 for _ in 0..10 {
1079 monitor.update_metrics(make_metrics_simple());
1080 }
1081 assert_eq!(monitor.metrics_history.len(), 5);
1082 }
1083
1084 #[test]
1085 fn test_training_monitor_get_recent_metrics() {
1086 let config = make_config();
1087 let mut monitor = TrainingMonitor::new(&config);
1088 for _ in 0..5 {
1089 monitor.update_metrics(make_metrics_simple());
1090 }
1091 let recent = monitor.get_recent_metrics(3);
1092 assert_eq!(recent.len(), 3);
1093 }
1094
1095 #[test]
1096 fn test_training_monitor_get_recent_metrics_more_than_available() {
1097 let config = make_config();
1098 let mut monitor = TrainingMonitor::new(&config);
1099 monitor.update_metrics(make_metrics_simple());
1100 let recent = monitor.get_recent_metrics(10);
1101 assert_eq!(recent.len(), 1);
1102 }
1103
1104 #[test]
1105 fn test_training_monitor_set_alert_thresholds() {
1106 let config = make_config();
1107 let mut monitor = TrainingMonitor::new(&config);
1108 let thresholds = AlertThresholds {
1109 loss_increase_threshold: 2.0,
1110 gradient_norm_max: 5.0,
1111 memory_usage_max_mb: 4096.0,
1112 gpu_utilization_min: 0.5,
1113 learning_rate_min: 1e-6,
1114 tokens_per_second_min: 50.0,
1115 };
1116 monitor.set_alert_thresholds(thresholds);
1117 assert!((monitor.alert_thresholds.gradient_norm_max - 5.0).abs() < 1e-9);
1118 }
1119
1120 #[test]
1121 fn test_training_monitor_gradient_explosion_alert() {
1122 let config = make_config();
1123 let mut monitor = TrainingMonitor::new(&config);
1124 let mut metrics = make_metrics_simple();
1125 metrics.gradient_norm = Some(100.0);
1126 monitor.update_metrics(metrics);
1127 assert!(monitor
1128 .active_alerts
1129 .iter()
1130 .any(|a| a.alert_type == AlertType::GradientExplosion));
1131 }
1132
1133 #[test]
1134 fn test_training_monitor_memory_overuse_alert() {
1135 let config = make_config();
1136 let mut monitor = TrainingMonitor::new(&config);
1137 let metrics = make_metrics_with(Some(0.5), Some(0.8), 10000.0);
1138 monitor.update_metrics(metrics);
1139 assert!(monitor.active_alerts.iter().any(|a| a.alert_type == AlertType::MemoryOveruse));
1140 }
1141
1142 #[test]
1143 fn test_training_monitor_low_gpu_alert() {
1144 let config = make_config();
1145 let mut monitor = TrainingMonitor::new(&config);
1146 let mut metrics = make_metrics_simple();
1147 metrics.gpu_utilization = Some(0.1);
1148 monitor.update_metrics(metrics);
1149 assert!(monitor
1150 .active_alerts
1151 .iter()
1152 .any(|a| a.alert_type == AlertType::LowGpuUtilization));
1153 }
1154
1155 #[test]
1156 fn test_training_monitor_slow_token_alert() {
1157 let config = make_config();
1158 let mut monitor = TrainingMonitor::new(&config);
1159 let mut metrics = make_metrics_simple();
1160 metrics.tokens_per_second = Some(10.0);
1161 monitor.update_metrics(metrics);
1162 assert!(monitor
1163 .active_alerts
1164 .iter()
1165 .any(|a| a.alert_type == AlertType::SlowTokenProcessing));
1166 }
1167
1168 #[test]
1169 fn test_training_monitor_no_duplicate_alerts() {
1170 let config = make_config();
1171 let mut monitor = TrainingMonitor::new(&config);
1172 let mut metrics = make_metrics_simple();
1173 metrics.gradient_norm = Some(100.0);
1174 monitor.update_metrics(metrics.clone());
1175 monitor.update_metrics(metrics);
1176 let grad_alerts = monitor
1177 .active_alerts
1178 .iter()
1179 .filter(|a| a.alert_type == AlertType::GradientExplosion)
1180 .count();
1181 assert_eq!(grad_alerts, 1);
1182 }
1183
1184 #[test]
1185 fn test_training_monitor_average_loss_none() {
1186 let config = make_config();
1187 let monitor = TrainingMonitor::new(&config);
1188 assert!(monitor.calculate_average_loss().is_none());
1189 }
1190
1191 #[test]
1192 fn test_training_monitor_average_loss() {
1193 let config = make_config();
1194 let mut monitor = TrainingMonitor::new(&config);
1195 monitor.update_metrics(make_metrics_with(Some(1.0), None, 1024.0));
1196 monitor.update_metrics(make_metrics_with(Some(2.0), None, 1024.0));
1197 let avg = monitor.calculate_average_loss();
1198 assert!(avg.is_some());
1199 assert!((avg.expect("should be some") - 1.5).abs() < 1e-9);
1200 }
1201
1202 #[test]
1203 fn test_training_monitor_best_accuracy_none() {
1204 let config = make_config();
1205 let monitor = TrainingMonitor::new(&config);
1206 assert!(monitor.calculate_best_accuracy().is_none());
1207 }
1208
1209 #[test]
1210 fn test_training_monitor_best_accuracy() {
1211 let config = make_config();
1212 let mut monitor = TrainingMonitor::new(&config);
1213 monitor.update_metrics(make_metrics_with(None, Some(0.7), 1024.0));
1214 monitor.update_metrics(make_metrics_with(None, Some(0.9), 1024.0));
1215 monitor.update_metrics(make_metrics_with(None, Some(0.8), 1024.0));
1216 let best = monitor.calculate_best_accuracy();
1217 assert!(best.is_some());
1218 assert!((best.expect("should be some") - 0.9).abs() < 1e-9);
1219 }
1220
1221 #[test]
1222 fn test_training_monitor_avg_tps_none() {
1223 let config = make_config();
1224 let monitor = TrainingMonitor::new(&config);
1225 assert!(monitor.calculate_average_tokens_per_second().is_none());
1226 }
1227
1228 #[test]
1229 fn test_training_stability_insufficient() {
1230 let config = make_config();
1231 let monitor = TrainingMonitor::new(&config);
1232 assert!(matches!(
1233 monitor.calculate_training_stability(),
1234 TrainingStability::Insufficient
1235 ));
1236 }
1237
1238 #[test]
1239 fn test_convergence_too_early() {
1240 let config = make_config();
1241 let monitor = TrainingMonitor::new(&config);
1242 assert!(matches!(
1243 monitor.assess_convergence(),
1244 ConvergenceStatus::TooEarly
1245 ));
1246 }
1247
1248 #[test]
1249 fn test_generate_training_summary() {
1250 let config = make_config();
1251 let monitor = TrainingMonitor::new(&config);
1252 let summary = monitor.generate_training_summary();
1253 assert_eq!(summary.total_steps, 0);
1254 assert!(matches!(
1255 summary.convergence_status,
1256 ConvergenceStatus::TooEarly
1257 ));
1258 }
1259
1260 #[test]
1263 fn test_model_comparator_new() {
1264 let comparator = ModelComparator::new();
1265 assert!(comparator.models.is_empty());
1266 }
1267
1268 #[test]
1269 fn test_model_comparator_add_model() {
1270 let mut comparator = ModelComparator::new();
1271 comparator.add_model(ModelMetrics {
1272 model_id: "m1".to_string(),
1273 model_name: "Model A".to_string(),
1274 metrics_history: Vec::new(),
1275 final_loss: Some(0.5),
1276 final_accuracy: Some(0.9),
1277 training_time: Duration::from_secs(100),
1278 parameter_count: 1000,
1279 model_size_mb: 10.0,
1280 });
1281 assert_eq!(comparator.models.len(), 1);
1282 }
1283
1284 #[test]
1285 fn test_model_comparator_find_best_model_empty() {
1286 let comparator = ModelComparator::new();
1287 assert!(comparator.find_best_model().is_none());
1288 }
1289
1290 #[test]
1291 fn test_model_comparator_find_best_model() {
1292 let mut comparator = ModelComparator::new();
1293 comparator.add_model(ModelMetrics {
1294 model_id: "m1".to_string(),
1295 model_name: "Model A".to_string(),
1296 metrics_history: Vec::new(),
1297 final_loss: Some(0.5),
1298 final_accuracy: Some(0.9),
1299 training_time: Duration::from_secs(100),
1300 parameter_count: 1000,
1301 model_size_mb: 10.0,
1302 });
1303 comparator.add_model(ModelMetrics {
1304 model_id: "m2".to_string(),
1305 model_name: "Model B".to_string(),
1306 metrics_history: Vec::new(),
1307 final_loss: Some(0.3),
1308 final_accuracy: Some(0.95),
1309 training_time: Duration::from_secs(200),
1310 parameter_count: 2000,
1311 model_size_mb: 20.0,
1312 });
1313 let best = comparator.find_best_model();
1314 assert!(best.is_some());
1315 assert_eq!(best.expect("should find best"), "m2");
1316 }
1317
1318 #[test]
1319 fn test_model_comparator_rank_models() {
1320 let mut comparator = ModelComparator::new();
1321 comparator.add_model(ModelMetrics {
1322 model_id: "m1".to_string(),
1323 model_name: "A".to_string(),
1324 metrics_history: Vec::new(),
1325 final_loss: Some(0.5),
1326 final_accuracy: None,
1327 training_time: Duration::from_secs(100),
1328 parameter_count: 1000,
1329 model_size_mb: 10.0,
1330 });
1331 let ranking = comparator.rank_models();
1332 assert_eq!(ranking.len(), 1);
1333 assert_eq!(ranking[0].rank, 1);
1334 }
1335
1336 #[test]
1337 fn test_model_comparator_generate_recommendation_similar() {
1338 let comparator = ModelComparator::new();
1339 let ma = ModelMetrics {
1340 model_id: "a".to_string(),
1341 model_name: "A".to_string(),
1342 metrics_history: Vec::new(),
1343 final_loss: Some(0.5),
1344 final_accuracy: None,
1345 training_time: Duration::from_secs(100),
1346 parameter_count: 1000,
1347 model_size_mb: 10.0,
1348 };
1349 let rec = comparator.generate_recommendation(&ma, &ma, 0.0);
1350 assert!(rec.contains("similarly"));
1351 }
1352
1353 #[test]
1356 fn test_hyperparameter_explorer_new() {
1357 let explorer = HyperparameterExplorer::new();
1358 assert!(explorer.experiments.is_empty());
1359 }
1360
1361 #[test]
1362 fn test_hyperparameter_explorer_add_experiment() {
1363 let mut explorer = HyperparameterExplorer::new();
1364 explorer.add_experiment(HyperparameterExperiment {
1365 experiment_id: "exp1".to_string(),
1366 hyperparameters: HashMap::new(),
1367 results: ExperimentResults {
1368 final_loss: Some(0.5),
1369 final_accuracy: Some(0.9),
1370 training_time: Duration::from_secs(100),
1371 convergence_epoch: Some(50),
1372 best_validation_score: Some(0.88),
1373 },
1374 status: ExperimentStatus::Completed,
1375 });
1376 assert_eq!(explorer.experiments.len(), 1);
1377 }
1378
1379 #[test]
1380 fn test_hyperparameter_explorer_get_recommendations() {
1381 let explorer = HyperparameterExplorer::new();
1382 let recs = explorer.get_recommendations();
1383 assert_eq!(recs.total_experiments, 0);
1384 assert!(!recs.parameter_importance.is_empty());
1385 }
1386
1387 #[test]
1388 fn test_hyperparameter_explorer_suggest_next_experiments() {
1389 let explorer = HyperparameterExplorer::new();
1390 let suggestions = explorer.suggest_next_experiments(3);
1391 assert_eq!(suggestions.len(), 3);
1392 }
1393
1394 #[test]
1397 fn test_interactive_dashboard_new() {
1398 let config = make_config();
1399 let dashboard = InteractiveDashboard::new(&config);
1400 assert!(dashboard.websocket_server.is_none());
1401 }
1402
1403 #[test]
1404 fn test_interactive_dashboard_update() {
1405 let config = make_config();
1406 let mut dashboard = InteractiveDashboard::new(&config);
1407 dashboard.update(make_metrics_simple());
1408 assert_eq!(dashboard.training_monitor.metrics_history.len(), 1);
1409 }
1410
1411 #[test]
1412 fn test_interactive_dashboard_snapshot() {
1413 let config = make_config();
1414 let dashboard = InteractiveDashboard::new(&config);
1415 let snapshot = dashboard.get_dashboard_snapshot();
1416 assert!(snapshot.recent_metrics.is_empty());
1417 }
1418
1419 #[test]
1420 fn test_interactive_dashboard_generate_recommendations() {
1421 let config = make_config();
1422 let dashboard = InteractiveDashboard::new(&config);
1423 let recs = dashboard.generate_recommendations();
1424 assert!(!recs.is_empty());
1425 }
1426
1427 #[test]
1428 fn test_interactive_dashboard_generate_key_insights() {
1429 let config = make_config();
1430 let dashboard = InteractiveDashboard::new(&config);
1431 let insights = dashboard.generate_key_insights();
1432 assert!(insights.is_empty() || !insights.is_empty());
1434 }
1435
1436 #[test]
1439 fn test_comparison_config_default() {
1440 let config = ComparisonConfig::default();
1441 assert_eq!(config.primary_metric, "loss");
1442 assert_eq!(config.comparison_window, 100);
1443 }
1444
1445 #[test]
1448 fn test_search_space_default() {
1449 let space = HyperparameterSearchSpace::default();
1450 assert!(space.learning_rate.0 < space.learning_rate.1);
1451 assert!(space.batch_size.0 < space.batch_size.1);
1452 }
1453}