1#![allow(dead_code)]
56
57use crate::averaged_adam::{AveragedAdam, AveragedAdamConfig};
58use crate::multinode::{MultiNodeConfig, MultiNodeTrainer};
59use crate::traits::StatefulOptimizer;
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::sync::{Arc, Mutex};
63use std::time::{Duration, Instant};
64use trustformers_core::errors::{Result, TrustformersError};
65use trustformers_core::parallel::CommunicationBackend;
66use trustformers_core::tensor::Tensor;
67use trustformers_core::traits::Optimizer;
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct DistributedConfig {
72 pub num_gpus: usize,
74 pub gpu_ids: Vec<usize>,
76 pub backend: CommunicationBackend,
78 pub compression: CompressionConfig,
80 pub dynamic_batching: DynamicBatchingConfig,
82 pub fault_tolerance: FaultToleranceConfig,
84 pub monitoring: MonitoringConfig,
86 pub memory_optimization: MemoryOptimizationConfig,
88}
89
90impl Default for DistributedConfig {
91 fn default() -> Self {
92 Self {
93 num_gpus: 1,
94 gpu_ids: vec![0],
95 backend: CommunicationBackend::Nccl,
96 compression: CompressionConfig::default(),
97 dynamic_batching: DynamicBatchingConfig::default(),
98 fault_tolerance: FaultToleranceConfig::default(),
99 monitoring: MonitoringConfig::default(),
100 memory_optimization: MemoryOptimizationConfig::default(),
101 }
102 }
103}
104
105impl DistributedConfig {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn with_gpus(mut self, num_gpus: usize) -> Self {
113 self.num_gpus = num_gpus;
114 self.gpu_ids = (0..num_gpus).collect();
115 self
116 }
117
118 pub fn with_gpu_ids(mut self, gpu_ids: Vec<usize>) -> Self {
120 self.num_gpus = gpu_ids.len();
121 self.gpu_ids = gpu_ids;
122 self
123 }
124
125 pub fn with_gradient_compression(mut self, compression_type: CompressionType) -> Self {
127 self.compression.enabled = true;
128 self.compression.algorithm = compression_type;
129 self
130 }
131
132 pub fn with_dynamic_batching(mut self, enabled: bool) -> Self {
134 self.dynamic_batching.enabled = enabled;
135 self
136 }
137
138 pub fn with_fault_tolerance(mut self, enabled: bool) -> Self {
140 self.fault_tolerance.enabled = enabled;
141 self
142 }
143
144 pub fn with_backend(mut self, backend: CommunicationBackend) -> Self {
146 self.backend = backend;
147 self
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub enum CompressionType {
154 None,
156 TopK { k: usize },
158 RandomSparsification { ratio: f32 },
160 Quantization { bits: u8 },
162 PowerSGD { rank: usize },
164 OneBitSGD,
166 Adaptive,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct CompressionConfig {
173 pub enabled: bool,
174 pub algorithm: CompressionType,
175 pub target_ratio: f32,
177 pub error_feedback: bool,
179 pub adaptive_threshold: f32,
181}
182
183impl Default for CompressionConfig {
184 fn default() -> Self {
185 Self {
186 enabled: false,
187 algorithm: CompressionType::TopK { k: 1000 },
188 target_ratio: 0.1,
189 error_feedback: true,
190 adaptive_threshold: 0.01,
191 }
192 }
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DynamicBatchingConfig {
198 pub enabled: bool,
199 pub initial_batch_size: usize,
201 pub min_batch_size: usize,
203 pub max_batch_size: usize,
205 pub target_utilization: f32,
207 pub adjustment_frequency: usize,
209}
210
211impl Default for DynamicBatchingConfig {
212 fn default() -> Self {
213 Self {
214 enabled: false,
215 initial_batch_size: 32,
216 min_batch_size: 8,
217 max_batch_size: 128,
218 target_utilization: 0.85,
219 adjustment_frequency: 100,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct FaultToleranceConfig {
227 pub enabled: bool,
228 pub checkpoint_frequency: usize,
230 pub max_retries: usize,
232 pub heartbeat_interval: Duration,
234 pub auto_replacement: bool,
236}
237
238impl Default for FaultToleranceConfig {
239 fn default() -> Self {
240 Self {
241 enabled: false,
242 checkpoint_frequency: 1000,
243 max_retries: 3,
244 heartbeat_interval: Duration::from_secs(10),
245 auto_replacement: false,
246 }
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct MonitoringConfig {
253 pub enabled: bool,
254 pub real_time_metrics: bool,
256 pub auto_tuning: bool,
258 pub collection_frequency: Duration,
260 pub bandwidth_monitoring: bool,
262}
263
264impl Default for MonitoringConfig {
265 fn default() -> Self {
266 Self {
267 enabled: true,
268 real_time_metrics: true,
269 auto_tuning: false,
270 collection_frequency: Duration::from_secs(1),
271 bandwidth_monitoring: true,
272 }
273 }
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct MemoryOptimizationConfig {
279 pub gradient_checkpointing: bool,
281 pub cpu_offloading: bool,
283 pub memory_pool_size_gb: f32,
285 pub auto_gc: bool,
287 pub memory_threshold: f32,
289}
290
291impl Default for MemoryOptimizationConfig {
292 fn default() -> Self {
293 Self {
294 gradient_checkpointing: false,
295 cpu_offloading: false,
296 memory_pool_size_gb: 4.0,
297 auto_gc: true,
298 memory_threshold: 0.9,
299 }
300 }
301}
302
303pub struct EnhancedDistributedTrainer<T: Optimizer + StatefulOptimizer> {
305 config: DistributedConfig,
306 optimizer: T,
307 multi_node_trainer: Option<MultiNodeTrainer<T>>,
308 performance_monitor: PerformanceMonitor,
309 gradient_compressor: GradientCompressor,
310 dynamic_batcher: DynamicBatcher,
311 fault_handler: FaultHandler,
312 step_count: usize,
313 start_time: Instant,
314 gpu_contexts: Vec<Arc<GpuContext>>,
315 parameter_registry: HashMap<String, ParameterInfo>,
316 reduced_gradients: HashMap<String, Tensor>,
321}
322
323#[derive(Debug)]
332pub struct GpuContext {
333 pub device_id: usize,
335 pub memory_usage: Arc<Mutex<Option<f32>>>,
337 pub utilization: Arc<Mutex<Option<f32>>>,
339 pub temperature: Arc<Mutex<Option<f32>>>,
341 pub communication_bandwidth: Arc<Mutex<Option<f32>>>,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq)]
351pub struct GpuTelemetrySample {
352 pub utilization: f32,
354 pub memory_usage: f32,
356 pub temperature_celsius: f32,
358 pub communication_bandwidth_mb_s: f32,
360}
361
362impl GpuTelemetrySample {
363 fn validate(&self, device_id: usize) -> Result<()> {
365 for (label, value, upper) in [
366 ("utilization", self.utilization, 1.0_f32),
367 ("memory_usage", self.memory_usage, 1.0),
368 ] {
369 if !value.is_finite() || !(0.0..=upper).contains(&value) {
370 return Err(TrustformersError::invalid_input(format!(
371 "GPU {device_id} telemetry `{label}` must be a finite fraction in [0, {upper}], got {value}"
372 )));
373 }
374 }
375 if !self.temperature_celsius.is_finite() {
376 return Err(TrustformersError::invalid_input(format!(
377 "GPU {device_id} telemetry `temperature_celsius` must be finite, got {}",
378 self.temperature_celsius
379 )));
380 }
381 if !self.communication_bandwidth_mb_s.is_finite() || self.communication_bandwidth_mb_s < 0.0
382 {
383 return Err(TrustformersError::invalid_input(format!(
384 "GPU {device_id} telemetry `communication_bandwidth_mb_s` must be finite and \
385 non-negative, got {}",
386 self.communication_bandwidth_mb_s
387 )));
388 }
389 Ok(())
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct ParameterInfo {
396 pub name: String,
397 pub shape: Vec<usize>,
398 pub size: usize,
399 pub device_id: usize,
400 pub is_sharded: bool,
401}
402
403#[derive(Debug, Clone)]
414pub struct PerformanceMetrics {
415 pub throughput: f32,
417 pub gpu_utilization: Vec<f32>,
419 pub memory_usage: Vec<f32>,
421 pub communication_overhead: f32,
423 pub compression_ratio: f32,
425 pub bandwidth_utilization: f32,
428 pub step_time: Duration,
430}
431
432pub struct PerformanceMonitor {
434 config: MonitoringConfig,
435 metrics_history: Vec<PerformanceMetrics>,
436 last_collection: Instant,
437 throughput_tracker: ThroughputTracker,
438}
439
440impl PerformanceMonitor {
441 pub fn new(config: MonitoringConfig) -> Self {
442 Self {
443 config,
444 metrics_history: Vec::new(),
445 last_collection: Instant::now(),
446 throughput_tracker: ThroughputTracker::new(),
447 }
448 }
449
450 fn read_metric(
455 gpu_contexts: &[Arc<GpuContext>],
456 select: impl Fn(&GpuContext) -> &Arc<Mutex<Option<f32>>>,
457 label: &str,
458 ) -> Result<Option<Vec<f32>>> {
459 let mut values = Vec::with_capacity(gpu_contexts.len());
460 for ctx in gpu_contexts {
461 let guard = select(ctx).lock().map_err(|_| {
462 TrustformersError::lock_error(format!("GPU context {label} mutex poisoned"))
463 })?;
464 match *guard {
465 Some(value) => values.push(value),
466 None => return Ok(None),
467 }
468 }
469 if values.is_empty() {
470 return Ok(None);
471 }
472 Ok(Some(values))
473 }
474
475 pub fn collect_metrics(
480 &mut self,
481 gpu_contexts: &[Arc<GpuContext>],
482 ) -> Result<PerformanceMetrics> {
483 let now = Instant::now();
484 let step_time = now - self.last_collection;
485 self.last_collection = now;
486
487 let gpu_utilization =
488 Self::read_metric(gpu_contexts, |ctx| &ctx.utilization, "utilization")?
489 .unwrap_or_default();
490
491 let memory_usage =
492 Self::read_metric(gpu_contexts, |ctx| &ctx.memory_usage, "memory_usage")?
493 .unwrap_or_default();
494
495 let bandwidth_utilization = match Self::read_metric(
496 gpu_contexts,
497 |ctx| &ctx.communication_bandwidth,
498 "communication_bandwidth",
499 )? {
500 Some(values) => values.iter().sum::<f32>() / values.len() as f32,
501 None => 0.0,
502 };
503
504 let throughput = self.throughput_tracker.calculate_throughput();
505
506 let metrics = PerformanceMetrics {
507 throughput,
508 gpu_utilization,
509 memory_usage,
510 communication_overhead: 0.0, compression_ratio: 0.0, bandwidth_utilization,
513 step_time,
514 };
515
516 self.metrics_history.push(metrics.clone());
517
518 if self.metrics_history.len() > 1000 {
520 self.metrics_history.drain(0..500);
521 }
522
523 Ok(metrics)
524 }
525
526 pub fn get_recent_metrics(&self, count: usize) -> &[PerformanceMetrics] {
527 let start = self.metrics_history.len().saturating_sub(count);
528 &self.metrics_history[start..]
529 }
530
531 pub fn analyze_performance_trends(&self) -> PerformanceAnalysis {
532 if self.metrics_history.len() < 10 {
533 return PerformanceAnalysis::default();
534 }
535
536 let recent_metrics = self.get_recent_metrics(100);
537
538 let avg_throughput =
539 recent_metrics.iter().map(|m| m.throughput).sum::<f32>() / recent_metrics.len() as f32;
540
541 let mut util_samples = 0usize;
545 let mut util_total = 0.0f32;
546 for m in recent_metrics {
547 if m.gpu_utilization.is_empty() {
548 continue;
549 }
550 util_total += m.gpu_utilization.iter().sum::<f32>() / m.gpu_utilization.len() as f32;
551 util_samples += 1;
552 }
553 let avg_gpu_util = if util_samples == 0 { 0.0 } else { util_total / util_samples as f32 };
554
555 let avg_comm_overhead =
556 recent_metrics.iter().map(|m| m.communication_overhead).sum::<f32>()
557 / recent_metrics.len() as f32;
558
559 PerformanceAnalysis {
560 average_throughput: avg_throughput,
561 average_gpu_utilization: avg_gpu_util,
562 average_communication_overhead: avg_comm_overhead,
563 performance_trend: self.calculate_trend(),
564 bottleneck_analysis: self.identify_bottlenecks(recent_metrics),
565 }
566 }
567
568 fn calculate_trend(&self) -> PerformanceTrend {
569 if self.metrics_history.len() < 20 {
570 return PerformanceTrend::Stable;
571 }
572
573 let recent = self.get_recent_metrics(10);
574 let older =
575 &self.metrics_history[self.metrics_history.len() - 20..self.metrics_history.len() - 10];
576
577 let recent_avg = recent.iter().map(|m| m.throughput).sum::<f32>() / recent.len() as f32;
578 let older_avg = older.iter().map(|m| m.throughput).sum::<f32>() / older.len() as f32;
579
580 if !older_avg.is_finite() || older_avg.abs() < f32::EPSILON {
583 return PerformanceTrend::Stable;
584 }
585 let change_ratio = (recent_avg - older_avg) / older_avg;
586
587 if change_ratio > 0.05 {
588 PerformanceTrend::Improving
589 } else if change_ratio < -0.05 {
590 PerformanceTrend::Degrading
591 } else {
592 PerformanceTrend::Stable
593 }
594 }
595
596 fn identify_bottlenecks(&self, metrics: &[PerformanceMetrics]) -> Vec<Bottleneck> {
597 let mut bottlenecks = Vec::new();
598 if metrics.is_empty() {
599 return bottlenecks;
600 }
601
602 for m in metrics.iter() {
605 for (gpu_id, &util) in m.gpu_utilization.iter().enumerate() {
606 if util < 0.7 {
607 bottlenecks.push(Bottleneck::LowGpuUtilization {
608 gpu_id,
609 utilization: util,
610 });
611 }
612 }
613 }
614
615 let avg_comm =
617 metrics.iter().map(|m| m.communication_overhead).sum::<f32>() / metrics.len() as f32;
618 if avg_comm > 0.3 {
619 bottlenecks.push(Bottleneck::HighCommunicationOverhead { overhead: avg_comm });
620 }
621
622 for m in metrics {
624 for (gpu_id, &memory) in m.memory_usage.iter().enumerate() {
625 if memory > 0.95 {
626 bottlenecks.push(Bottleneck::HighMemoryUsage {
627 gpu_id,
628 usage: memory,
629 });
630 }
631 }
632 }
633
634 bottlenecks
635 }
636}
637
638#[derive(Debug, Clone)]
639pub struct PerformanceAnalysis {
640 pub average_throughput: f32,
641 pub average_gpu_utilization: f32,
642 pub average_communication_overhead: f32,
643 pub performance_trend: PerformanceTrend,
644 pub bottleneck_analysis: Vec<Bottleneck>,
645}
646
647impl Default for PerformanceAnalysis {
648 fn default() -> Self {
649 Self {
650 average_throughput: 0.0,
651 average_gpu_utilization: 0.0,
652 average_communication_overhead: 0.0,
653 performance_trend: PerformanceTrend::Stable,
654 bottleneck_analysis: Vec::new(),
655 }
656 }
657}
658
659#[derive(Debug, Clone)]
660pub enum PerformanceTrend {
661 Improving,
662 Stable,
663 Degrading,
664}
665
666#[derive(Debug, Clone)]
667pub enum Bottleneck {
668 LowGpuUtilization { gpu_id: usize, utilization: f32 },
669 HighCommunicationOverhead { overhead: f32 },
670 HighMemoryUsage { gpu_id: usize, usage: f32 },
671 InsufficientBandwidth { bandwidth_mbps: f32 },
672}
673
674pub struct ThroughputTracker {
676 sample_count: usize,
677 start_time: Instant,
678 last_reset: Instant,
679}
680
681impl Default for ThroughputTracker {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687impl ThroughputTracker {
688 pub fn new() -> Self {
689 let now = Instant::now();
690 Self {
691 sample_count: 0,
692 start_time: now,
693 last_reset: now,
694 }
695 }
696
697 pub fn record_samples(&mut self, count: usize) {
698 self.sample_count += count;
699 }
700
701 pub fn calculate_throughput(&self) -> f32 {
702 let elapsed = self.last_reset.elapsed().as_secs_f32();
703 if elapsed > 0.0 {
704 self.sample_count as f32 / elapsed
705 } else {
706 0.0
707 }
708 }
709
710 pub fn reset(&mut self) {
711 self.sample_count = 0;
712 self.last_reset = Instant::now();
713 }
714}
715
716pub mod compression;
717
718pub use compression::{CompressedData, CompressedGradient, CompressionStats, GradientCompressor};
719
720pub struct DynamicBatcher {
722 config: DynamicBatchingConfig,
723 current_batch_sizes: Vec<usize>,
724 utilization_history: Vec<Vec<f32>>,
725 adjustment_counter: usize,
726}
727
728impl DynamicBatcher {
729 pub fn new(config: DynamicBatchingConfig, num_gpus: usize) -> Self {
730 let current_batch_sizes = vec![config.initial_batch_size; num_gpus];
731 Self {
732 config,
733 current_batch_sizes,
734 utilization_history: Vec::new(),
735 adjustment_counter: 0,
736 }
737 }
738
739 pub fn get_batch_sizes(&self) -> &[usize] {
740 &self.current_batch_sizes
741 }
742
743 pub fn update_batch_sizes(&mut self, gpu_utilizations: &[f32]) -> Result<bool> {
744 if !self.config.enabled {
745 return Ok(false);
746 }
747
748 self.utilization_history.push(gpu_utilizations.to_vec());
749 self.adjustment_counter += 1;
750
751 if self.adjustment_counter < self.config.adjustment_frequency {
752 return Ok(false);
753 }
754
755 self.adjustment_counter = 0;
757
758 let avg_utilizations = self.calculate_average_utilizations();
760 let mut adjusted = false;
761
762 let tracked = avg_utilizations.len().min(self.current_batch_sizes.len());
766 for (gpu_id, &avg_util) in avg_utilizations.iter().enumerate().take(tracked) {
767 let current_batch = self.current_batch_sizes[gpu_id];
768 let new_batch = if avg_util < self.config.target_utilization - 0.05 {
769 (current_batch + 8).min(self.config.max_batch_size)
771 } else if avg_util > self.config.target_utilization + 0.05 {
772 (current_batch.saturating_sub(8)).max(self.config.min_batch_size)
774 } else {
775 current_batch
776 };
777
778 if new_batch != current_batch {
779 self.current_batch_sizes[gpu_id] = new_batch;
780 adjusted = true;
781
782 log::debug!(
783 "GPU {}: adjusted batch size {} -> {} (utilization: {:.1}%)",
784 gpu_id,
785 current_batch,
786 new_batch,
787 avg_util * 100.0
788 );
789 }
790 }
791
792 if self.utilization_history.len() > 1000 {
794 self.utilization_history.drain(0..500);
795 }
796
797 Ok(adjusted)
798 }
799
800 fn calculate_average_utilizations(&self) -> Vec<f32> {
801 if self.utilization_history.is_empty() {
802 return vec![0.0; self.current_batch_sizes.len()];
803 }
804
805 let num_gpus = self.current_batch_sizes.len();
806 let mut sums = vec![0.0; num_gpus];
807 let mut counts = vec![0; num_gpus];
808
809 for utilizations in &self.utilization_history {
810 for (i, &util) in utilizations.iter().enumerate() {
811 if i < num_gpus {
812 sums[i] += util;
813 counts[i] += 1;
814 }
815 }
816 }
817
818 sums.into_iter()
819 .zip(counts)
820 .map(|(sum, count)| if count > 0 { sum / count as f32 } else { 0.0 })
821 .collect()
822 }
823}
824
825type RecoveryPolicy = Box<dyn FnMut(usize) -> Result<bool> + Send>;
827
828pub struct FaultHandler {
830 config: FaultToleranceConfig,
831 failed_nodes: Vec<usize>,
832 checkpoint_manager: CheckpointManager,
833 heartbeat_tracker: HeartbeatTracker,
834 recovery_policy: Option<RecoveryPolicy>,
835}
836
837impl std::fmt::Debug for FaultHandler {
838 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
839 f.debug_struct("FaultHandler")
840 .field("config", &self.config)
841 .field("failed_nodes", &self.failed_nodes)
842 .field("has_recovery_policy", &self.recovery_policy.is_some())
843 .finish()
844 }
845}
846
847impl FaultHandler {
848 pub fn new(config: FaultToleranceConfig) -> Self {
849 let checkpoint_frequency = config.checkpoint_frequency;
850 let heartbeat_interval = config.heartbeat_interval;
851
852 Self {
853 config,
854 failed_nodes: Vec::new(),
855 checkpoint_manager: CheckpointManager::new(checkpoint_frequency),
856 heartbeat_tracker: HeartbeatTracker::new(heartbeat_interval),
857 recovery_policy: None,
858 }
859 }
860
861 pub fn should_checkpoint(&self, step: usize) -> bool {
862 step.is_multiple_of(self.config.checkpoint_frequency)
863 }
864
865 pub fn handle_node_failure(&mut self, node_id: usize) -> Result<bool> {
866 if !self.config.enabled {
867 return Ok(false);
868 }
869
870 self.failed_nodes.push(node_id);
871 log::warn!("node {} failed, attempting recovery", node_id);
872
873 if self.config.auto_replacement {
874 self.recover_from_failure(node_id)
876 } else {
877 Ok(false)
878 }
879 }
880
881 fn recover_from_failure(&mut self, node_id: usize) -> Result<bool> {
893 match self.recovery_policy.as_mut() {
894 Some(policy) => {
895 let recovered = policy(node_id)?;
896 if recovered {
897 log::info!("recovery policy reported node {node_id} recovered");
898 } else {
899 log::warn!("recovery policy could not recover node {node_id}");
900 }
901 Ok(recovered)
902 },
903 None => Err(TrustformersError::not_implemented(
904 "automatic node recovery: FaultHandler owns neither the process group nor the \
905 model state, so it cannot re-form the communicator or reload a checkpoint. \
906 Install a policy with FaultHandler::set_recovery_policy, or disable \
907 FaultToleranceConfig::auto_replacement and handle the failure in the training \
908 loop"
909 .to_string(),
910 )),
911 }
912 }
913
914 pub fn set_recovery_policy<F>(&mut self, policy: F)
920 where
921 F: FnMut(usize) -> Result<bool> + Send + 'static,
922 {
923 self.recovery_policy = Some(Box::new(policy));
924 }
925}
926
927pub struct CheckpointManager {
929 frequency: usize,
930 last_checkpoint: usize,
931}
932
933impl CheckpointManager {
934 pub fn new(frequency: usize) -> Self {
935 Self {
936 frequency,
937 last_checkpoint: 0,
938 }
939 }
940
941 pub fn should_save(&self, step: usize) -> bool {
942 step - self.last_checkpoint >= self.frequency
943 }
944}
945
946pub struct HeartbeatTracker {
948 interval: Duration,
949 last_heartbeat: HashMap<usize, Instant>,
950}
951
952impl HeartbeatTracker {
953 pub fn new(interval: Duration) -> Self {
954 Self {
955 interval,
956 last_heartbeat: HashMap::new(),
957 }
958 }
959
960 pub fn record_heartbeat(&mut self, node_id: usize) {
961 self.last_heartbeat.insert(node_id, Instant::now());
962 }
963
964 pub fn check_failed_nodes(&self) -> Vec<usize> {
965 let now = Instant::now();
966 self.last_heartbeat
967 .iter()
968 .filter_map(|(&node_id, &last_time)| {
969 if now - last_time > self.interval * 3 {
970 Some(node_id)
972 } else {
973 None
974 }
975 })
976 .collect()
977 }
978}
979
980impl<T: Optimizer + StatefulOptimizer + Clone> EnhancedDistributedTrainer<T> {
981 pub fn new(config: DistributedConfig, optimizer: T) -> Result<Self> {
983 let gpu_contexts = config
985 .gpu_ids
986 .iter()
987 .map(|&id| {
988 Arc::new(GpuContext {
989 device_id: id,
990 memory_usage: Arc::new(Mutex::new(None)),
992 utilization: Arc::new(Mutex::new(None)),
993 temperature: Arc::new(Mutex::new(None)),
994 communication_bandwidth: Arc::new(Mutex::new(None)),
995 })
996 })
997 .collect();
998
999 let multi_node_trainer = if config.num_gpus > 1 {
1001 let multi_config = MultiNodeConfig {
1002 num_nodes: 1,
1003 devices_per_node: config.num_gpus,
1004 node_rank: 0,
1005 local_rank: 0,
1006 global_rank: 0,
1007 zero_config: Default::default(),
1008 gradient_compression: config.compression.enabled,
1009 comm_backend: config.backend,
1010 overlap_comm_compute: true,
1011 gradient_bucket_size_mb: 25,
1012 };
1013 Some(MultiNodeTrainer::new(multi_config, optimizer.clone())?)
1014 } else {
1015 None
1016 };
1017
1018 Ok(Self {
1019 config: config.clone(),
1020 optimizer,
1021 multi_node_trainer,
1022 performance_monitor: PerformanceMonitor::new(config.monitoring),
1023 gradient_compressor: GradientCompressor::new(config.compression),
1024 dynamic_batcher: DynamicBatcher::new(config.dynamic_batching, config.num_gpus),
1025 fault_handler: FaultHandler::new(config.fault_tolerance),
1026 step_count: 0,
1027 start_time: Instant::now(),
1028 gpu_contexts,
1029 parameter_registry: HashMap::new(),
1030 reduced_gradients: HashMap::new(),
1031 })
1032 }
1033
1034 pub fn register_model(&mut self, parameters: HashMap<String, Tensor>) -> Result<()> {
1036 if let Some(ref mut trainer) = self.multi_node_trainer {
1038 trainer.register_parameters(parameters.clone())?;
1039 }
1040
1041 for (name, tensor) in parameters {
1043 let param_info = ParameterInfo {
1044 name: name.clone(),
1045 shape: tensor.shape().to_vec(),
1046 size: tensor.shape().iter().product(),
1047 device_id: 0, is_sharded: false,
1049 };
1050 self.parameter_registry.insert(name, param_info);
1051 }
1052
1053 log::info!(
1054 "registered {} parameters for distributed training",
1055 self.parameter_registry.len()
1056 );
1057 Ok(())
1058 }
1059
1060 pub fn train_step(&mut self, gradients: HashMap<String, Tensor>) -> Result<TrainingStepResult> {
1075 let step_start = Instant::now();
1076
1077 let compressed_gradients = self.gradient_compressor.compress_gradients(&gradients)?;
1079
1080 let batch_size_adjusted = match self.recorded_gpu_utilizations()? {
1082 Some(utilizations) => self.dynamic_batcher.update_batch_sizes(&utilizations)?,
1083 None => {
1084 log::debug!(
1085 "skipping dynamic batch sizing: no GPU telemetry recorded (call \
1086 EnhancedDistributedTrainer::record_gpu_telemetry)"
1087 );
1088 false
1089 },
1090 };
1091
1092 let mut decompressed: HashMap<String, Tensor> =
1094 HashMap::with_capacity(compressed_gradients.len());
1095 for (name, compressed) in &compressed_gradients {
1096 decompressed.insert(name.clone(), compressed.decompress()?);
1097 }
1098
1099 if let Some(ref mut trainer) = self.multi_node_trainer {
1100 trainer.update_gradients(decompressed.clone())?;
1101 trainer.optimizer_step()?;
1102 }
1103 self.reduced_gradients = decompressed;
1104
1105 self.step_count += 1;
1106
1107 if self.fault_handler.should_checkpoint(self.step_count) {
1113 log::info!(
1114 "checkpoint interval reached at step {}; call \
1115 SmartCheckpointManager::create_checkpoint with the model state",
1116 self.step_count
1117 );
1118 }
1119
1120 let performance_metrics = self.performance_monitor.collect_metrics(&self.gpu_contexts)?;
1122
1123 let step_time = step_start.elapsed();
1124
1125 Ok(TrainingStepResult {
1126 step: self.step_count,
1127 step_time,
1128 compression_ratio: self
1129 .gradient_compressor
1130 .get_compression_stats()
1131 .average_compression_ratio,
1132 batch_size_adjusted,
1133 performance_metrics,
1134 })
1135 }
1136
1137 pub fn record_gpu_telemetry(
1151 &mut self,
1152 device_id: usize,
1153 sample: GpuTelemetrySample,
1154 ) -> Result<()> {
1155 sample.validate(device_id)?;
1156
1157 let ctx =
1158 self.gpu_contexts.iter().find(|ctx| ctx.device_id == device_id).ok_or_else(|| {
1159 TrustformersError::invalid_input(format!(
1160 "device {device_id} is not part of this trainer; configured devices: {:?}",
1161 self.gpu_contexts.iter().map(|ctx| ctx.device_id).collect::<Vec<_>>()
1162 ))
1163 })?;
1164
1165 let store = |slot: &Arc<Mutex<Option<f32>>>, value: f32, label: &str| -> Result<()> {
1166 let mut guard = slot.lock().map_err(|_| {
1167 TrustformersError::lock_error(format!("GPU context {label} mutex poisoned"))
1168 })?;
1169 *guard = Some(value);
1170 Ok(())
1171 };
1172
1173 store(&ctx.utilization, sample.utilization, "utilization")?;
1174 store(&ctx.memory_usage, sample.memory_usage, "memory_usage")?;
1175 store(&ctx.temperature, sample.temperature_celsius, "temperature")?;
1176 store(
1177 &ctx.communication_bandwidth,
1178 sample.communication_bandwidth_mb_s,
1179 "communication_bandwidth",
1180 )?;
1181 Ok(())
1182 }
1183
1184 fn recorded_gpu_utilizations(&self) -> Result<Option<Vec<f32>>> {
1187 let mut values = Vec::with_capacity(self.gpu_contexts.len());
1188 for ctx in &self.gpu_contexts {
1189 let guard = ctx.utilization.lock().map_err(|_| {
1190 TrustformersError::lock_error("GPU context utilization mutex poisoned".to_string())
1191 })?;
1192 match *guard {
1193 Some(value) => values.push(value),
1194 None => return Ok(None),
1195 }
1196 }
1197 Ok(if values.is_empty() { None } else { Some(values) })
1198 }
1199
1200 pub fn take_reduced_gradients(&mut self) -> HashMap<String, Tensor> {
1203 std::mem::take(&mut self.reduced_gradients)
1204 }
1205
1206 pub fn reduced_gradients(&self) -> &HashMap<String, Tensor> {
1209 &self.reduced_gradients
1210 }
1211
1212 pub fn apply_reduced_gradients(
1223 &mut self,
1224 parameters: &mut HashMap<String, Tensor>,
1225 ) -> Result<usize> {
1226 let mut names: Vec<String> = self.reduced_gradients.keys().cloned().collect();
1227 names.sort();
1228
1229 for name in &names {
1230 let gradient = self.reduced_gradients.get(name).ok_or_else(|| {
1231 TrustformersError::invalid_input(format!("gradient `{name}` vanished"))
1232 })?;
1233 let parameter = parameters.get_mut(name).ok_or_else(|| {
1234 TrustformersError::invalid_input(format!(
1235 "no parameter named `{name}` to apply its gradient to"
1236 ))
1237 })?;
1238 self.optimizer.update(parameter, gradient)?;
1239 }
1240 self.optimizer.step();
1241 Ok(names.len())
1242 }
1243
1244 pub fn get_training_stats(&self) -> DistributedTrainingStats {
1249 let performance_analysis = self.performance_monitor.analyze_performance_trends();
1250 let compression_stats = self.gradient_compressor.get_compression_stats();
1251
1252 let collect_known = |select: fn(&GpuContext) -> &Arc<Mutex<Option<f32>>>| -> Vec<f32> {
1253 let mut values = Vec::with_capacity(self.gpu_contexts.len());
1254 for ctx in &self.gpu_contexts {
1255 match *select(ctx).lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
1256 Some(value) => values.push(value),
1257 None => return Vec::new(),
1258 }
1259 }
1260 values
1261 };
1262
1263 let memory_usage: Vec<f32> = collect_known(|ctx| &ctx.memory_usage);
1264 let gpu_utilization: Vec<f32> = collect_known(|ctx| &ctx.utilization);
1265
1266 DistributedTrainingStats {
1267 total_steps: self.step_count,
1268 training_time: self.start_time.elapsed(),
1269 average_throughput: performance_analysis.average_throughput,
1270 gpu_utilization,
1271 memory_usage,
1272 compression_ratio: compression_stats.average_compression_ratio,
1273 communication_overhead: performance_analysis.average_communication_overhead,
1274 batch_sizes: self.dynamic_batcher.get_batch_sizes().to_vec(),
1275 failed_nodes: self.fault_handler.failed_nodes.clone(),
1276 performance_trend: performance_analysis.performance_trend,
1277 bottlenecks: performance_analysis.bottleneck_analysis,
1278 }
1279 }
1280
1281 pub fn training_stats_report(&self) -> String {
1286 use std::fmt::Write as _;
1287
1288 let stats = self.get_training_stats();
1289 let mut report = String::new();
1290
1291 let _ = writeln!(report, "Enhanced distributed training statistics");
1294 let _ = writeln!(report, "Training progress:");
1295 let _ = writeln!(report, " total steps: {}", stats.total_steps);
1296 let _ = writeln!(
1297 report,
1298 " training time: {:.2} minutes",
1299 stats.training_time.as_secs_f32() / 60.0
1300 );
1301 let _ = writeln!(
1302 report,
1303 " average throughput: {:.1} samples/sec",
1304 stats.average_throughput
1305 );
1306
1307 let _ = writeln!(report, "GPU performance:");
1308 for (index, (&utilization, &memory)) in
1309 stats.gpu_utilization.iter().zip(&stats.memory_usage).enumerate()
1310 {
1311 let _ = writeln!(
1312 report,
1313 " GPU {}: utilization {:.1}%, memory {:.1}%",
1314 index,
1315 utilization * 100.0,
1316 memory * 100.0
1317 );
1318 }
1319
1320 let _ = writeln!(report, "Optimization metrics:");
1321 let _ = writeln!(
1322 report,
1323 " compression ratio: {:.1}%",
1324 stats.compression_ratio * 100.0
1325 );
1326 let _ = writeln!(
1327 report,
1328 " communication overhead: {:.1}%",
1329 stats.communication_overhead * 100.0
1330 );
1331 let _ = writeln!(report, " performance trend: {:?}", stats.performance_trend);
1332
1333 if !stats.bottlenecks.is_empty() {
1334 let _ = writeln!(report, "Identified bottlenecks:");
1335 for bottleneck in &stats.bottlenecks {
1336 match bottleneck {
1337 Bottleneck::LowGpuUtilization {
1338 gpu_id,
1339 utilization,
1340 } => {
1341 let _ = writeln!(
1342 report,
1343 " - GPU {} low utilization: {:.1}%",
1344 gpu_id,
1345 utilization * 100.0
1346 );
1347 },
1348 Bottleneck::HighCommunicationOverhead { overhead } => {
1349 let _ = writeln!(
1350 report,
1351 " - high communication overhead: {:.1}%",
1352 overhead * 100.0
1353 );
1354 },
1355 Bottleneck::HighMemoryUsage { gpu_id, usage } => {
1356 let _ = writeln!(
1357 report,
1358 " - GPU {} high memory usage: {:.1}%",
1359 gpu_id,
1360 usage * 100.0
1361 );
1362 },
1363 Bottleneck::InsufficientBandwidth { bandwidth_mbps } => {
1364 let _ = writeln!(
1365 report,
1366 " - insufficient bandwidth: {:.0} Mbps",
1367 bandwidth_mbps
1368 );
1369 },
1370 }
1371 }
1372 }
1373
1374 report
1375 }
1376
1377 pub fn print_training_stats(&self) {
1384 println!("{}", self.training_stats_report());
1385 }
1386
1387 pub fn log_training_stats(&self) {
1390 log::info!("{}", self.training_stats_report());
1391 }
1392
1393 pub fn checkpoint_due(&self) -> bool {
1397 self.fault_handler.should_checkpoint(self.step_count)
1398 }
1399
1400 pub fn optimize_hyperparameters(&mut self) -> Result<T> {
1416 if self.config.monitoring.auto_tuning {
1417 return Err(TrustformersError::not_implemented(
1418 "distributed hyperparameter optimization: \
1419 EnhancedDistributedTrainer has no trial-evaluation callback, so no search can \
1420 be run. Drive crate::hyperparameter_tuning::HyperparameterTuner directly with \
1421 your own objective function, or disable config.monitoring.auto_tuning"
1422 .to_string(),
1423 ));
1424 }
1425
1426 Ok(self.optimizer.clone())
1427 }
1428}
1429
1430#[derive(Debug, Clone)]
1432pub struct TrainingStepResult {
1433 pub step: usize,
1434 pub step_time: Duration,
1435 pub compression_ratio: f32,
1436 pub batch_size_adjusted: bool,
1437 pub performance_metrics: PerformanceMetrics,
1438}
1439
1440#[derive(Debug, Clone)]
1442pub struct DistributedTrainingStats {
1443 pub total_steps: usize,
1444 pub training_time: Duration,
1445 pub average_throughput: f32,
1446 pub gpu_utilization: Vec<f32>,
1447 pub memory_usage: Vec<f32>,
1448 pub compression_ratio: f32,
1449 pub communication_overhead: f32,
1450 pub batch_sizes: Vec<usize>,
1451 pub failed_nodes: Vec<usize>,
1452 pub performance_trend: PerformanceTrend,
1453 pub bottlenecks: Vec<Bottleneck>,
1454}
1455
1456impl AveragedAdam {
1458 pub fn for_distributed_training() -> Self {
1460 let config = AveragedAdamConfig {
1461 lr: 1e-3,
1462 betas: (0.9, 0.999),
1463 eps: 1e-8,
1464 weight_decay: 0.01,
1465 averaging_coeff: 0.9999, use_averaged: true,
1467 averaging_warmup: 1000, };
1469
1470 AveragedAdam::new(
1471 config.lr,
1472 config.betas,
1473 config.eps,
1474 config.weight_decay,
1475 config.averaging_coeff,
1476 )
1477 }
1478
1479 pub fn for_large_scale_distributed(world_size: usize) -> Self {
1481 let lr_scale = (world_size as f32).sqrt();
1483 let config = AveragedAdamConfig {
1484 lr: 1e-3 * lr_scale,
1485 betas: (0.9, 0.999),
1486 eps: 1e-8,
1487 weight_decay: 0.01 / lr_scale, averaging_coeff: 1.0 - (1.0 - 0.999) / world_size as f32, use_averaged: true,
1490 averaging_warmup: 1000 + world_size * 10, };
1492
1493 AveragedAdam::new(
1494 config.lr,
1495 config.betas,
1496 config.eps,
1497 config.weight_decay,
1498 config.averaging_coeff,
1499 )
1500 }
1501}
1502
1503#[cfg(test)]
1504mod tests {
1505 use super::*;
1506 use crate::adam::Adam;
1507
1508 #[test]
1509 fn test_distributed_config_creation() {
1510 let config = DistributedConfig::new()
1511 .with_gpus(4)
1512 .with_gradient_compression(CompressionType::TopK { k: 1000 })
1513 .with_dynamic_batching(true)
1514 .with_fault_tolerance(true);
1515
1516 assert_eq!(config.num_gpus, 4);
1517 assert_eq!(config.gpu_ids, vec![0, 1, 2, 3]);
1518 assert!(config.compression.enabled);
1519 assert!(config.dynamic_batching.enabled);
1520 assert!(config.fault_tolerance.enabled);
1521 }
1522
1523 #[test]
1524 fn test_gradient_compression() {
1525 let config = CompressionConfig {
1526 enabled: true,
1527 algorithm: CompressionType::TopK { k: 5 },
1528 target_ratio: 0.1,
1529 error_feedback: false,
1530 adaptive_threshold: 0.01,
1531 };
1532
1533 let mut compressor = GradientCompressor::new(config);
1534 let gradient = Tensor::ones(&[10]).expect("Failed to create tensor");
1535 let mut gradients = HashMap::new();
1536 gradients.insert("test".to_string(), gradient);
1537
1538 let compressed =
1539 compressor.compress_gradients(&gradients).expect("Operation failed in test");
1540 assert!(compressed.contains_key("test"));
1541
1542 let compressed_grad = &compressed["test"];
1543 assert!(compressed_grad.compression_ratio <= 1.0);
1544 }
1545
1546 #[test]
1547 fn test_performance_monitor() {
1548 let config = MonitoringConfig::default();
1549 let mut monitor = PerformanceMonitor::new(config);
1550
1551 let gpu_contexts = vec![Arc::new(GpuContext {
1552 device_id: 0,
1553 memory_usage: Arc::new(Mutex::new(Some(0.8))),
1554 utilization: Arc::new(Mutex::new(Some(0.9))),
1555 temperature: Arc::new(Mutex::new(Some(75.0))),
1556 communication_bandwidth: Arc::new(Mutex::new(Some(1000.0))),
1557 })];
1558
1559 let metrics = monitor.collect_metrics(&gpu_contexts).expect("Operation failed in test");
1560 assert_eq!(metrics.gpu_utilization, vec![0.9]);
1561 assert_eq!(metrics.memory_usage, vec![0.8]);
1562 assert_eq!(metrics.bandwidth_utilization, 1000.0);
1563 }
1564
1565 #[test]
1570 fn unsampled_devices_report_no_telemetry() {
1571 let mut monitor = PerformanceMonitor::new(MonitoringConfig::default());
1572 let gpu_contexts = vec![Arc::new(GpuContext {
1573 device_id: 0,
1574 memory_usage: Arc::new(Mutex::new(None)),
1575 utilization: Arc::new(Mutex::new(None)),
1576 temperature: Arc::new(Mutex::new(None)),
1577 communication_bandwidth: Arc::new(Mutex::new(None)),
1578 })];
1579
1580 let metrics = monitor.collect_metrics(&gpu_contexts).expect("collect must succeed in test");
1581 assert!(
1582 metrics.gpu_utilization.is_empty(),
1583 "unknown utilization must stay empty, got {:?}",
1584 metrics.gpu_utilization
1585 );
1586 assert!(metrics.memory_usage.is_empty());
1587 assert_eq!(metrics.bandwidth_utilization, 0.0);
1588 }
1589
1590 #[test]
1591 fn train_step_does_not_invent_gpu_telemetry() {
1592 let config = DistributedConfig::new().with_gpus(1);
1593 let optimizer = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
1594 let mut trainer =
1595 EnhancedDistributedTrainer::new(config, optimizer).expect("trainer must build in test");
1596
1597 let mut gradients = HashMap::new();
1598 gradients.insert(
1599 "w".to_string(),
1600 Tensor::from_slice(&[0.1f32, -0.2, 0.3], &[3]).expect("tensor must build in test"),
1601 );
1602
1603 let result = trainer.train_step(gradients).expect("train step must succeed in test");
1604 assert!(
1605 result.performance_metrics.gpu_utilization.is_empty(),
1606 "no telemetry was recorded, so none may be reported: {:?}",
1607 result.performance_metrics.gpu_utilization
1608 );
1609 assert!(!result.batch_size_adjusted);
1610 }
1611
1612 #[test]
1613 fn recorded_telemetry_is_reported_verbatim() {
1614 let config = DistributedConfig::new().with_gpu_ids(vec![3]);
1615 let optimizer = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
1616 let mut trainer =
1617 EnhancedDistributedTrainer::new(config, optimizer).expect("trainer must build in test");
1618
1619 trainer
1620 .record_gpu_telemetry(
1621 3,
1622 GpuTelemetrySample {
1623 utilization: 0.42,
1624 memory_usage: 0.17,
1625 temperature_celsius: 61.5,
1626 communication_bandwidth_mb_s: 512.0,
1627 },
1628 )
1629 .expect("recording telemetry must succeed in test");
1630
1631 let stats = trainer.get_training_stats();
1632 assert_eq!(stats.gpu_utilization, vec![0.42]);
1633 assert_eq!(stats.memory_usage, vec![0.17]);
1634
1635 assert!(trainer
1637 .record_gpu_telemetry(
1638 9,
1639 GpuTelemetrySample {
1640 utilization: 0.5,
1641 memory_usage: 0.5,
1642 temperature_celsius: 50.0,
1643 communication_bandwidth_mb_s: 1.0,
1644 },
1645 )
1646 .is_err());
1647
1648 assert!(trainer
1650 .record_gpu_telemetry(
1651 3,
1652 GpuTelemetrySample {
1653 utilization: 1.5,
1654 memory_usage: 0.5,
1655 temperature_celsius: 50.0,
1656 communication_bandwidth_mb_s: 1.0,
1657 },
1658 )
1659 .is_err());
1660 }
1661
1662 #[test]
1665 fn train_step_gradients_reach_the_optimizer() {
1666 let config = DistributedConfig::new().with_gpus(1);
1667 let optimizer = Adam::new(0.1, (0.9, 0.999), 1e-8, 0.0);
1668 let mut trainer =
1669 EnhancedDistributedTrainer::new(config, optimizer).expect("trainer must build in test");
1670
1671 let mut parameters = HashMap::new();
1672 parameters.insert(
1673 "w".to_string(),
1674 Tensor::from_slice(&[1.0f32, 2.0, 3.0], &[3]).expect("tensor must build in test"),
1675 );
1676 let before = parameters["w"].to_vec_f32().expect("tensor read must succeed in test");
1677
1678 let mut gradients = HashMap::new();
1679 gradients.insert(
1680 "w".to_string(),
1681 Tensor::from_slice(&[0.5f32, 0.5, 0.5], &[3]).expect("tensor must build in test"),
1682 );
1683
1684 trainer.train_step(gradients).expect("train step must succeed in test");
1685 assert_eq!(trainer.reduced_gradients().len(), 1);
1686
1687 let updated = trainer
1688 .apply_reduced_gradients(&mut parameters)
1689 .expect("applying gradients must succeed in test");
1690 assert_eq!(updated, 1);
1691
1692 let after = parameters["w"].to_vec_f32().expect("tensor read must succeed in test");
1693 assert_ne!(before, after, "a positive gradient must move the parameter");
1694 for (old, new) in before.iter().zip(&after) {
1695 assert!(
1696 new < old,
1697 "descent must decrease each weight: {old} -> {new}"
1698 );
1699 }
1700 }
1701
1702 #[test]
1705 fn node_recovery_requires_a_real_policy() {
1706 let mut handler = FaultHandler::new(FaultToleranceConfig {
1707 enabled: true,
1708 checkpoint_frequency: 10,
1709 max_retries: 3,
1710 heartbeat_interval: Duration::from_secs(1),
1711 auto_replacement: true,
1712 });
1713
1714 assert!(
1715 handler.handle_node_failure(2).is_err(),
1716 "no recovery policy is installed, so recovery cannot be claimed"
1717 );
1718
1719 handler.set_recovery_policy(|node_id| Ok(node_id != 7));
1720 assert!(handler.handle_node_failure(2).expect("policy must run in test"));
1721 assert!(!handler.handle_node_failure(7).expect("policy must run in test"));
1722 }
1723
1724 #[test]
1725 fn test_dynamic_batcher() {
1726 let config = DynamicBatchingConfig {
1727 enabled: true,
1728 initial_batch_size: 32,
1729 min_batch_size: 8,
1730 max_batch_size: 128,
1731 target_utilization: 0.8,
1732 adjustment_frequency: 1, };
1734
1735 let mut batcher = DynamicBatcher::new(config, 2);
1736 assert_eq!(batcher.get_batch_sizes(), &[32, 32]);
1737
1738 let low_utilization = vec![0.5, 0.6];
1740 let _adjusted =
1741 batcher.update_batch_sizes(&low_utilization).expect("Operation failed in test");
1742
1743 let final_sizes = batcher.get_batch_sizes();
1746 assert_eq!(final_sizes.len(), 2);
1747 }
1748
1749 #[test]
1750 fn test_averaged_adam_distributed_config() {
1751 let _optimizer = AveragedAdam::for_distributed_training();
1752 }
1755
1756 #[test]
1757 fn test_enhanced_distributed_trainer_creation() {
1758 let config = DistributedConfig::new().with_gpus(1);
1759 let optimizer = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
1760
1761 let trainer = EnhancedDistributedTrainer::new(config, optimizer)
1765 .expect("single-device trainer must build in test");
1766 assert_eq!(trainer.config.num_gpus, 1);
1767 assert_eq!(trainer.step_count, 0);
1768 }
1769
1770 #[test]
1771 fn optimize_hyperparameters_reports_not_implemented_instead_of_a_success_banner() {
1772 let mut config = DistributedConfig::new().with_gpus(1);
1775 config.monitoring.auto_tuning = true;
1776 let optimizer = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
1777
1778 let mut trainer = EnhancedDistributedTrainer::new(config, optimizer)
1779 .expect("single-device trainer must build in test");
1780
1781 let Err(error) = trainer.optimize_hyperparameters() else {
1782 panic!("auto-tuning must not report success without running a search in test");
1783 };
1784 let message = error.to_string();
1785 assert!(
1786 message.contains("hyperparameter") && message.contains("not implemented")
1787 || message.contains("HyperparameterTuner"),
1788 "unexpected error: {message}"
1789 );
1790 }
1791
1792 #[test]
1793 fn optimize_hyperparameters_is_a_no_op_when_auto_tuning_is_off() {
1794 let mut config = DistributedConfig::new().with_gpus(1);
1795 config.monitoring.auto_tuning = false;
1796 let optimizer = Adam::new(0.001, (0.9, 0.999), 1e-8, 0.0);
1797
1798 let mut trainer = EnhancedDistributedTrainer::new(config, optimizer)
1799 .expect("single-device trainer must build in test");
1800
1801 assert!(trainer.optimize_hyperparameters().is_ok());
1804 }
1805}