Skip to main content

trustformers_optim/
enhanced_distributed_training.rs

1//! # Enhanced Multi-GPU Distributed Training Framework
2//!
3//! This module provides advanced distributed training capabilities building upon
4//! the existing multi-node infrastructure with focus on:
5//! - Modern GPU communication patterns (NCCL integration)
6//! - Advanced gradient compression and quantization
7//! - Dynamic load balancing and fault tolerance
8//! - Integration with cutting-edge optimizers (Averaged Adam, etc.)
9//! - Real-time performance monitoring and auto-tuning
10//!
11//! ## Key Features
12//!
13//! 1. **GPU-Optimized Communication**: NCCL-based all-reduce with topology awareness
14//! 2. **Advanced Gradient Compression**: Multiple compression algorithms with adaptive selection
15//! 3. **Dynamic Load Balancing**: Automatic workload redistribution based on GPU performance
16//! 4. **Fault Tolerance**: Automatic recovery from node failures with checkpoint restoration
17//! 5. **Performance Auto-Tuning**: Real-time optimization of batch sizes and communication patterns
18//!
19//! ## Usage Example
20//!
21//! ```rust,no_run
22//! use trustformers_optim::{AveragedAdam, CompressionType, DistributedConfig, EnhancedDistributedTrainer};
23//! # use std::collections::HashMap;
24//! # use trustformers_core::tensor::Tensor;
25//!
26//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! // Create distributed configuration
28//! let config = DistributedConfig::new()
29//!     .with_gpus(8)
30//!     .with_gradient_compression(CompressionType::PowerSGD { rank: 4 })
31//!     .with_dynamic_batching(true)
32//!     .with_fault_tolerance(true);
33//!
34//! // Initialize Averaged Adam for distributed training
35//! let optimizer = AveragedAdam::for_distributed_training();
36//!
37//! // Create enhanced distributed trainer
38//! let mut trainer = EnhancedDistributedTrainer::new(config, optimizer)?;
39//!
40//! // Register model parameters
41//! # let model_parameters: HashMap<String, Tensor> = HashMap::new();
42//! trainer.register_model(model_parameters)?;
43//!
44//! // Training loop with automatic optimization
45//! # let data_loader: Vec<HashMap<String, Tensor>> = Vec::new();
46//! for batch in data_loader {
47//!     trainer.train_step(batch)?;
48//! }
49//! # Ok(())
50//! # }
51//! ```
52
53// reason: research-stage module — reserved API/scaffolding fields and methods
54// retained intentionally for in-progress features; not yet on active call paths.
55#![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/// Enhanced distributed training configuration with modern GPU optimizations
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct DistributedConfig {
72    /// Number of GPUs to use
73    pub num_gpus: usize,
74    /// GPU device IDs to use
75    pub gpu_ids: Vec<usize>,
76    /// Communication backend (NCCL preferred for GPUs)
77    pub backend: CommunicationBackend,
78    /// Gradient compression configuration
79    pub compression: CompressionConfig,
80    /// Dynamic batching configuration
81    pub dynamic_batching: DynamicBatchingConfig,
82    /// Fault tolerance settings
83    pub fault_tolerance: FaultToleranceConfig,
84    /// Performance monitoring settings
85    pub monitoring: MonitoringConfig,
86    /// Memory optimization settings
87    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    /// Create new distributed configuration
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Set number of GPUs
112    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    /// Set specific GPU IDs
119    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    /// Enable gradient compression
126    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    /// Enable dynamic batching
133    pub fn with_dynamic_batching(mut self, enabled: bool) -> Self {
134        self.dynamic_batching.enabled = enabled;
135        self
136    }
137
138    /// Enable fault tolerance
139    pub fn with_fault_tolerance(mut self, enabled: bool) -> Self {
140        self.fault_tolerance.enabled = enabled;
141        self
142    }
143
144    /// Set communication backend
145    pub fn with_backend(mut self, backend: CommunicationBackend) -> Self {
146        self.backend = backend;
147        self
148    }
149}
150
151/// Gradient compression algorithms for efficient communication
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub enum CompressionType {
154    /// No compression (baseline)
155    None,
156    /// Top-K sparsification
157    TopK { k: usize },
158    /// Random sparsification
159    RandomSparsification { ratio: f32 },
160    /// Quantization to lower precision
161    Quantization { bits: u8 },
162    /// PowerSGD low-rank compression
163    PowerSGD { rank: usize },
164    /// 1-Bit SGD compression
165    OneBitSGD,
166    /// Adaptive compression based on gradient statistics
167    Adaptive,
168}
169
170/// Gradient compression configuration
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct CompressionConfig {
173    pub enabled: bool,
174    pub algorithm: CompressionType,
175    /// Compression ratio target (0.1 = 90% reduction)
176    pub target_ratio: f32,
177    /// Enable error feedback for compression
178    pub error_feedback: bool,
179    /// Adaptive compression threshold
180    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/// Dynamic batching configuration for load balancing
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DynamicBatchingConfig {
198    pub enabled: bool,
199    /// Initial batch size per GPU
200    pub initial_batch_size: usize,
201    /// Minimum batch size
202    pub min_batch_size: usize,
203    /// Maximum batch size
204    pub max_batch_size: usize,
205    /// Target GPU utilization percentage
206    pub target_utilization: f32,
207    /// Batch size adjustment frequency (steps)
208    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/// Fault tolerance configuration
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct FaultToleranceConfig {
227    pub enabled: bool,
228    /// Checkpoint frequency (steps)
229    pub checkpoint_frequency: usize,
230    /// Maximum number of retries for failed operations
231    pub max_retries: usize,
232    /// Heartbeat interval for node health monitoring
233    pub heartbeat_interval: Duration,
234    /// Enable automatic node replacement
235    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/// Performance monitoring configuration
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct MonitoringConfig {
253    pub enabled: bool,
254    /// Enable real-time performance metrics
255    pub real_time_metrics: bool,
256    /// Enable automatic performance tuning
257    pub auto_tuning: bool,
258    /// Metrics collection frequency
259    pub collection_frequency: Duration,
260    /// Enable bandwidth monitoring
261    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/// Memory optimization configuration
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct MemoryOptimizationConfig {
279    /// Enable gradient checkpointing
280    pub gradient_checkpointing: bool,
281    /// Enable offloading to CPU memory
282    pub cpu_offloading: bool,
283    /// Memory pool size for efficient allocation
284    pub memory_pool_size_gb: f32,
285    /// Enable automatic garbage collection
286    pub auto_gc: bool,
287    /// Memory usage threshold for triggering optimizations
288    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
303/// Enhanced distributed trainer with modern GPU optimizations
304pub 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    /// Gradients produced by the last [`EnhancedDistributedTrainer::train_step`]
317    /// after compression, reduction and decompression, kept so the owner of the
318    /// parameter tensors can apply them (see
319    /// [`EnhancedDistributedTrainer::apply_reduced_gradients`]).
320    reduced_gradients: HashMap<String, Tensor>,
321}
322
323/// GPU context for managing device-specific operations.
324///
325/// Every metric is `None` until a **real** sample is supplied through
326/// [`EnhancedDistributedTrainer::record_gpu_telemetry`]. The pure-Rust default
327/// build links no GPU runtime (NVML/ROCm SMI are C libraries), so this crate
328/// cannot query a device itself. An earlier revision filled these fields with
329/// `0.8 + random()` and friends, which made every downstream decision — dynamic
330/// batch sizing, bottleneck detection, auto-scaling — act on invented data.
331#[derive(Debug)]
332pub struct GpuContext {
333    /// Device index this context tracks.
334    pub device_id: usize,
335    /// Fraction of device memory in use, in `[0, 1]`; `None` when unknown.
336    pub memory_usage: Arc<Mutex<Option<f32>>>,
337    /// Device utilization in `[0, 1]`; `None` when unknown.
338    pub utilization: Arc<Mutex<Option<f32>>>,
339    /// Device temperature in degrees Celsius; `None` when unknown.
340    pub temperature: Arc<Mutex<Option<f32>>>,
341    /// Achieved interconnect bandwidth in MB/s; `None` when unknown.
342    pub communication_bandwidth: Arc<Mutex<Option<f32>>>,
343}
344
345/// One real telemetry reading for a single device.
346///
347/// The embedder is responsible for obtaining these numbers (NVML, ROCm SMI,
348/// `nvidia-smi`, a cluster metrics endpoint, …) and feeding them in with
349/// [`EnhancedDistributedTrainer::record_gpu_telemetry`].
350#[derive(Debug, Clone, Copy, PartialEq)]
351pub struct GpuTelemetrySample {
352    /// Device utilization as a fraction in `[0, 1]`.
353    pub utilization: f32,
354    /// Fraction of device memory in use, in `[0, 1]`.
355    pub memory_usage: f32,
356    /// Device temperature in degrees Celsius.
357    pub temperature_celsius: f32,
358    /// Achieved interconnect bandwidth in MB/s.
359    pub communication_bandwidth_mb_s: f32,
360}
361
362impl GpuTelemetrySample {
363    /// Validate the ranges a caller can get wrong silently.
364    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/// Parameter information for distributed training
394#[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/// Performance metrics for distributed training.
404///
405/// # Device telemetry is opt-in
406///
407/// `gpu_utilization`, `memory_usage` and `bandwidth_utilization` are derived
408/// from samples the embedder recorded with
409/// [`EnhancedDistributedTrainer::record_gpu_telemetry`]. When a device has never
410/// been sampled there is nothing to report, and this crate does **not** invent a
411/// number: the two vectors are then left **empty** and `bandwidth_utilization`
412/// is `0.0`. Treat an empty vector as "unknown", never as "0% utilized".
413#[derive(Debug, Clone)]
414pub struct PerformanceMetrics {
415    /// Samples per second, measured by the throughput tracker.
416    pub throughput: f32,
417    /// Per-GPU utilization in `[0, 1]`; empty when no telemetry was recorded.
418    pub gpu_utilization: Vec<f32>,
419    /// Per-GPU memory usage in `[0, 1]`; empty when no telemetry was recorded.
420    pub memory_usage: Vec<f32>,
421    /// Fraction of step time spent communicating.
422    pub communication_overhead: f32,
423    /// Compression ratio achieved by the gradient codec.
424    pub compression_ratio: f32,
425    /// Mean interconnect bandwidth in MB/s; `0.0` when no telemetry was
426    /// recorded.
427    pub bandwidth_utilization: f32,
428    /// Wall-clock time of the training step.
429    pub step_time: Duration,
430}
431
432/// Real-time performance monitoring
433pub 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    /// Read one `Option<f32>` metric from every context.
451    ///
452    /// Returns `None` unless *every* device has a recorded sample: a partially
453    /// populated vector would silently misalign device indices with values.
454    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    /// Collect one metrics sample.
476    ///
477    /// Device-derived fields are populated only from telemetry the embedder
478    /// recorded; see [`PerformanceMetrics`] for the "unknown" encoding.
479    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, // Will be calculated based on timing
511            compression_ratio: 0.0,      // Will be set by compression module
512            bandwidth_utilization,
513            step_time,
514        };
515
516        self.metrics_history.push(metrics.clone());
517
518        // Keep only recent metrics
519        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        // Only samples that actually carry device telemetry contribute; an
542        // empty `gpu_utilization` means "unknown", and averaging it in as 0.0
543        // (or dividing by zero) would manufacture a utilization figure.
544        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        // A zero (or non-finite) baseline carries no trend information; saying
581        // "stable" is honest, dividing by it would produce inf/NaN.
582        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        // Check GPU utilization. Samples without recorded telemetry carry empty
603        // vectors and are therefore skipped rather than reported as "0% used".
604        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        // Check communication overhead
616        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        // Check memory usage
623        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
674/// Throughput tracking utility
675pub 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
720/// Dynamic batching for optimal GPU utilization
721pub 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        // Reset counter
756        self.adjustment_counter = 0;
757
758        // Calculate average utilization for each GPU
759        let avg_utilizations = self.calculate_average_utilizations();
760        let mut adjusted = false;
761
762        // The history may carry more entries than this batcher has devices (a
763        // caller can pass a longer slice); indexing beyond `current_batch_sizes`
764        // would panic, so the shorter of the two bounds the loop.
765        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                // Utilization too low - increase batch size
770                (current_batch + 8).min(self.config.max_batch_size)
771            } else if avg_util > self.config.target_utilization + 0.05 {
772                // Utilization too high - decrease batch size
773                (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        // Clear old history
793        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
825/// Callback that attempts to bring the job back after a node loss.
826type RecoveryPolicy = Box<dyn FnMut(usize) -> Result<bool> + Send>;
827
828/// Fault tolerance handler for robust distributed training
829pub 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            // Attempt to restore from checkpoint and continue training
875            self.recover_from_failure(node_id)
876        } else {
877            Ok(false)
878        }
879    }
880
881    /// Attempt to recover from the loss of `node_id`.
882    ///
883    /// Recovery means re-forming the communicator over the surviving nodes,
884    /// restoring the latest checkpoint and redistributing the workload — none
885    /// of which this handler can do on its own: it owns neither the process
886    /// group nor the model state. It therefore either delegates to the recovery
887    /// policy the embedder installed with
888    /// [`FaultHandler::set_recovery_policy`], or reports
889    /// [`TrustformersError::not_implemented`]. Returning `Ok(true)` without
890    /// having recovered anything would tell the training loop it is safe to
891    /// continue on a broken communicator.
892    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    /// Install the callback invoked when a node fails and
915    /// [`FaultToleranceConfig::auto_replacement`] is enabled.
916    ///
917    /// The callback receives the failed node id and returns whether training can
918    /// continue.
919    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
927/// Checkpoint management for fault tolerance
928pub 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
946/// Heartbeat tracking for node health monitoring
947pub 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                    // Allow 3x interval before marking as failed
971                    Some(node_id)
972                } else {
973                    None
974                }
975            })
976            .collect()
977    }
978}
979
980impl<T: Optimizer + StatefulOptimizer + Clone> EnhancedDistributedTrainer<T> {
981    /// Create new enhanced distributed trainer
982    pub fn new(config: DistributedConfig, optimizer: T) -> Result<Self> {
983        // Initialize GPU contexts
984        let gpu_contexts = config
985            .gpu_ids
986            .iter()
987            .map(|&id| {
988                Arc::new(GpuContext {
989                    device_id: id,
990                    // No telemetry until the embedder records some.
991                    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        // Create multi-node trainer if needed
1000        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    /// Register model parameters for distributed training
1035    pub fn register_model(&mut self, parameters: HashMap<String, Tensor>) -> Result<()> {
1036        // Register parameters with multi-node trainer if available
1037        if let Some(ref mut trainer) = self.multi_node_trainer {
1038            trainer.register_parameters(parameters.clone())?;
1039        }
1040
1041        // Build parameter registry
1042        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, // Simplified device assignment
1048                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    /// Perform one training step with enhanced distributed optimizations.
1061    ///
1062    /// The step compresses `gradients` with the configured codec, reduces them
1063    /// across the multi-node group when one is configured, and decompresses the
1064    /// result. The reduced gradients are retained; because this trainer holds
1065    /// parameter *metadata* only (see [`ParameterInfo`]), the caller — who owns
1066    /// the parameter tensors — applies them with
1067    /// [`EnhancedDistributedTrainer::apply_reduced_gradients`] or reads them via
1068    /// [`EnhancedDistributedTrainer::take_reduced_gradients`].
1069    ///
1070    /// Dynamic batch sizing runs only when device telemetry has been recorded
1071    /// for every device (see
1072    /// [`EnhancedDistributedTrainer::record_gpu_telemetry`]); without it there
1073    /// is nothing to base a resize on and the batch sizes are left alone.
1074    pub fn train_step(&mut self, gradients: HashMap<String, Tensor>) -> Result<TrainingStepResult> {
1075        let step_start = Instant::now();
1076
1077        // Compress gradients
1078        let compressed_gradients = self.gradient_compressor.compress_gradients(&gradients)?;
1079
1080        // Update dynamic batch sizes when real utilization samples exist.
1081        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        // Reduce and decompress, then keep the result for the parameter owner.
1093        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        // Signal that a checkpoint is due. This trainer holds parameter
1108        // *metadata* only (see `parameter_registry`), so it cannot serialize
1109        // model state itself; the owner drives
1110        // `SmartCheckpointManager::create_checkpoint` with the real tensors.
1111        // Claiming "checkpoint saved" here would be a fabrication.
1112        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        // Collect performance metrics
1121        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    /// Record a real telemetry reading for one device.
1138    ///
1139    /// This crate cannot read a GPU itself: NVML and ROCm SMI are C libraries
1140    /// and the default build is pure Rust. Rather than inventing plausible
1141    /// numbers, every device metric stays `None` until the embedder calls this
1142    /// with values it obtained from the platform. Metrics gathered here drive
1143    /// dynamic batch sizing, bottleneck detection and auto-scaling, so a
1144    /// fabricated sample would propagate into real training decisions.
1145    ///
1146    /// # Errors
1147    ///
1148    /// Fails when `device_id` is not one of the configured devices or when the
1149    /// sample is out of range (see [`GpuTelemetrySample`]).
1150    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    /// Utilization of every configured device, or `None` when at least one
1185    /// device has never been sampled.
1186    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    /// Take the gradients produced by the last
1201    /// [`EnhancedDistributedTrainer::train_step`], leaving the trainer empty.
1202    pub fn take_reduced_gradients(&mut self) -> HashMap<String, Tensor> {
1203        std::mem::take(&mut self.reduced_gradients)
1204    }
1205
1206    /// Borrow the gradients produced by the last
1207    /// [`EnhancedDistributedTrainer::train_step`].
1208    pub fn reduced_gradients(&self) -> &HashMap<String, Tensor> {
1209        &self.reduced_gradients
1210    }
1211
1212    /// Apply the gradients from the last [`EnhancedDistributedTrainer::train_step`]
1213    /// to `parameters` with this trainer's optimizer.
1214    ///
1215    /// Parameters are visited in sorted-name order so every rank performs the
1216    /// same update sequence. Returns the number of parameters updated.
1217    ///
1218    /// # Errors
1219    ///
1220    /// Fails when a gradient has no matching parameter — a silent skip would
1221    /// leave part of the model un-trained without any signal.
1222    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    /// Get comprehensive training statistics.
1245    ///
1246    /// `gpu_utilization` and `memory_usage` are empty when no device telemetry
1247    /// has been recorded; see [`EnhancedDistributedTrainer::record_gpu_telemetry`].
1248    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    /// Render the training statistics as a human-readable report.
1282    ///
1283    /// Prefer this over [`Self::print_training_stats`] inside libraries: it
1284    /// returns the text instead of writing to stdout.
1285    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        // Writing into a String is infallible, so the results are discarded
1292        // deliberately rather than unwrapped.
1293        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    /// Write [`Self::training_stats_report`] to stdout.
1378    ///
1379    /// This is an explicit, caller-initiated escape hatch for binaries and
1380    /// examples; nothing on the training path writes to stdout. Library callers
1381    /// should prefer [`Self::log_training_stats`], which routes the same report
1382    /// through the `log` facade so the host application controls the sink.
1383    pub fn print_training_stats(&self) {
1384        println!("{}", self.training_stats_report());
1385    }
1386
1387    /// Emit [`Self::training_stats_report`] at `info` level through the `log`
1388    /// facade.
1389    pub fn log_training_stats(&self) {
1390        log::info!("{}", self.training_stats_report());
1391    }
1392
1393    /// Whether the fault handler considers a checkpoint due at the current
1394    /// step. The caller owns the model state and drives
1395    /// [`crate::advanced_distributed_features::SmartCheckpointManager`].
1396    pub fn checkpoint_due(&self) -> bool {
1397        self.fault_handler.should_checkpoint(self.step_count)
1398    }
1399
1400    /// Optimize hyperparameters for the current distributed setup.
1401    ///
1402    /// # Errors
1403    ///
1404    /// Distributed-aware hyperparameter optimization is **not implemented**.
1405    /// The crate ships [`crate::hyperparameter_tuning`], but wiring it here
1406    /// requires an evaluation callback (a way to run a trial and score it) that
1407    /// this trainer does not have. Rather than returning an unmodified clone of
1408    /// the optimizer while reporting success, this returns
1409    /// [`TrustformersError`] describing what is missing whenever auto-tuning is
1410    /// requested.
1411    ///
1412    /// With `config.monitoring.auto_tuning == false` the call is a no-op and
1413    /// returns the current optimizer unchanged, which is honest: no
1414    /// optimization was requested and none was performed.
1415    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/// Result of a training step
1431#[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/// Comprehensive distributed training statistics
1441#[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
1456// Extension trait for Averaged Adam distributed training
1457impl AveragedAdam {
1458    /// Create Averaged Adam configuration optimized for distributed training
1459    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, // Higher averaging for distributed stability
1466            use_averaged: true,
1467            averaging_warmup: 1000, // Longer warmup for distributed training
1468        };
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    /// Create configuration for large-scale distributed training
1480    pub fn for_large_scale_distributed(world_size: usize) -> Self {
1481        // Adjust hyperparameters based on world size
1482        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, // Reduce weight decay for larger batch sizes
1488            averaging_coeff: 1.0 - (1.0 - 0.999) / world_size as f32, // Adjust averaging
1489            use_averaged: true,
1490            averaging_warmup: 1000 + world_size * 10, // Scale warmup with world size
1491        };
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    /// Regression: an earlier revision filled every device metric with
1566    /// `0.8 + random()` on each step, so callers received invented telemetry.
1567    /// Without a recorded sample the metrics must now be *absent*, never
1568    /// plausible-looking noise.
1569    #[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        // An unknown device is rejected instead of being silently created.
1636        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        // Out-of-range samples are rejected.
1649        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    /// Regression: the single-device branch of `train_step` used to decompress
1663    /// each gradient into `_grad` and drop it, so training was a no-op.
1664    #[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    /// Regression: `recover_from_failure` used to log and return `Ok(true)`,
1703    /// telling the training loop the job had recovered when nothing happened.
1704    #[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, // Adjust every step for testing
1733        };
1734
1735        let mut batcher = DynamicBatcher::new(config, 2);
1736        assert_eq!(batcher.get_batch_sizes(), &[32, 32]);
1737
1738        // Simulate low utilization
1739        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        // Should increase batch sizes due to low utilization
1744        // Note: May not adjust on first call due to frequency requirements
1745        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        // Test that it creates a valid configuration
1753        // In actual implementation, would verify specific parameters
1754    }
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        // A single-device trainer needs no external runtime, so construction
1762        // must succeed unconditionally; swallowing the error here would hide a
1763        // real regression.
1764        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        // The previous implementation printed "✅ Hyperparameter optimization
1773        // completed (placeholder)" and returned a clone of the input optimizer.
1774        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        // No optimization was requested, so returning the current optimizer
1802        // unchanged is the honest answer.
1803        assert!(trainer.optimize_hyperparameters().is_ok());
1804    }
1805}