Skip to main content

oxirs_embed/
training.rs

1//! Training utilities and advanced optimizers for embedding models
2
3use crate::{EmbeddingModel, TrainingStats};
4use anyhow::Result;
5use scirs2_core::ndarray_ext::Array2;
6use std::collections::{HashMap, VecDeque};
7use std::sync::{Arc, Mutex};
8use std::time::Instant;
9use tokio::sync::{broadcast, RwLock};
10use tokio::task::JoinHandle;
11use tracing::{debug, info, warn};
12
13/// Advanced training scheduler with various optimization strategies
14pub struct TrainingScheduler {
15    pub config: TrainingConfig,
16    pub optimizer: OptimizerType,
17    pub scheduler: LearningRateScheduler,
18    pub early_stopping: Option<EarlyStopping>,
19}
20
21/// Training configuration
22#[derive(Debug, Clone)]
23pub struct TrainingConfig {
24    pub max_epochs: usize,
25    pub batch_size: usize,
26    pub learning_rate: f64,
27    pub validation_freq: usize,
28    pub checkpoint_freq: usize,
29    pub log_freq: usize,
30    pub use_early_stopping: bool,
31    pub patience: usize,
32    pub min_delta: f64,
33}
34
35impl Default for TrainingConfig {
36    fn default() -> Self {
37        Self {
38            max_epochs: 1000,
39            batch_size: 1024,
40            learning_rate: 0.01,
41            validation_freq: 10,
42            checkpoint_freq: 100,
43            log_freq: 10,
44            use_early_stopping: true,
45            patience: 50,
46            min_delta: 1e-6,
47        }
48    }
49}
50
51/// Optimizer types
52#[derive(Debug, Clone)]
53pub enum OptimizerType {
54    SGD,
55    Adam {
56        beta1: f64,
57        beta2: f64,
58        epsilon: f64,
59    },
60    AdaGrad {
61        epsilon: f64,
62    },
63    RMSprop {
64        alpha: f64,
65        epsilon: f64,
66    },
67}
68
69impl Default for OptimizerType {
70    fn default() -> Self {
71        OptimizerType::Adam {
72            beta1: 0.9,
73            beta2: 0.999,
74            epsilon: 1e-8,
75        }
76    }
77}
78
79/// Learning rate scheduler
80#[derive(Debug, Clone)]
81pub enum LearningRateScheduler {
82    Constant,
83    ExponentialDecay {
84        decay_rate: f64,
85        decay_steps: usize,
86    },
87    StepDecay {
88        step_size: usize,
89        gamma: f64,
90    },
91    CosineAnnealing {
92        t_max: usize,
93        eta_min: f64,
94    },
95    ReduceOnPlateau {
96        factor: f64,
97        patience: usize,
98        threshold: f64,
99    },
100}
101
102impl Default for LearningRateScheduler {
103    fn default() -> Self {
104        LearningRateScheduler::ExponentialDecay {
105            decay_rate: 0.96,
106            decay_steps: 100,
107        }
108    }
109}
110
111impl LearningRateScheduler {
112    pub fn get_lr(&self, epoch: usize, base_lr: f64, _current_loss: Option<f64>) -> f64 {
113        match self {
114            LearningRateScheduler::Constant => base_lr,
115            LearningRateScheduler::ExponentialDecay {
116                decay_rate,
117                decay_steps,
118            } => base_lr * decay_rate.powf(epoch as f64 / *decay_steps as f64),
119            LearningRateScheduler::StepDecay { step_size, gamma } => {
120                base_lr * gamma.powf((epoch / step_size) as f64)
121            }
122            LearningRateScheduler::CosineAnnealing { t_max, eta_min } => {
123                eta_min
124                    + (base_lr - eta_min)
125                        * (1.0 + (std::f64::consts::PI * epoch as f64 / *t_max as f64).cos())
126                        / 2.0
127            }
128            LearningRateScheduler::ReduceOnPlateau { .. } => {
129                // This would require state tracking, simplified for now
130                base_lr
131            }
132        }
133    }
134}
135
136/// Early stopping implementation
137#[derive(Debug, Clone)]
138pub struct EarlyStopping {
139    patience: usize,
140    min_delta: f64,
141    best_loss: f64,
142    wait_count: usize,
143    stopped: bool,
144}
145
146impl EarlyStopping {
147    pub fn new(patience: usize, min_delta: f64) -> Self {
148        Self {
149            patience,
150            min_delta,
151            best_loss: f64::INFINITY,
152            wait_count: 0,
153            stopped: false,
154        }
155    }
156
157    pub fn update(&mut self, current_loss: f64) -> bool {
158        if current_loss < self.best_loss - self.min_delta {
159            self.best_loss = current_loss;
160            self.wait_count = 0;
161        } else {
162            self.wait_count += 1;
163            if self.wait_count > self.patience {
164                self.stopped = true;
165            }
166        }
167
168        self.stopped
169    }
170
171    pub fn should_stop(&self) -> bool {
172        self.stopped
173    }
174}
175
176/// Adam optimizer state
177#[derive(Debug, Clone)]
178pub struct AdamOptimizer {
179    beta1: f64,
180    beta2: f64,
181    epsilon: f64,
182    t: usize,               // time step
183    m: Option<Array2<f64>>, // first moment
184    v: Option<Array2<f64>>, // second moment
185}
186
187impl AdamOptimizer {
188    pub fn new(beta1: f64, beta2: f64, epsilon: f64) -> Self {
189        Self {
190            beta1,
191            beta2,
192            epsilon,
193            t: 0,
194            m: None,
195            v: None,
196        }
197    }
198
199    pub fn update(&mut self, params: &mut Array2<f64>, grads: &Array2<f64>, lr: f64) {
200        self.t += 1;
201
202        // Initialize moments if needed
203        if self.m.is_none() {
204            self.m = Some(Array2::zeros(params.raw_dim()));
205            self.v = Some(Array2::zeros(params.raw_dim()));
206        }
207
208        let m = self
209            .m
210            .as_mut()
211            .expect("moment estimate m should be initialized");
212        let v = self
213            .v
214            .as_mut()
215            .expect("moment estimate v should be initialized");
216
217        // Update biased first moment estimate
218        *m = &*m * self.beta1 + grads * (1.0 - self.beta1);
219
220        // Update biased second raw moment estimate
221        *v = &*v * self.beta2 + &(grads * grads) * (1.0 - self.beta2);
222
223        // Compute bias-corrected first moment estimate
224        let m_hat = &*m / (1.0 - self.beta1.powi(self.t as i32));
225
226        // Compute bias-corrected second raw moment estimate
227        let v_hat = &*v / (1.0 - self.beta2.powi(self.t as i32));
228
229        // Update parameters
230        *params = &*params - &(&m_hat / (&v_hat.mapv(|x| x.sqrt()) + self.epsilon)) * lr;
231    }
232}
233
234/// Training metrics tracker
235#[derive(Debug, Clone)]
236pub struct MetricsTracker {
237    pub losses: Vec<f64>,
238    pub learning_rates: Vec<f64>,
239    pub epochs: Vec<usize>,
240    pub validation_losses: Vec<f64>,
241    pub training_times: Vec<f64>,
242}
243
244impl MetricsTracker {
245    pub fn new() -> Self {
246        Self {
247            losses: Vec::new(),
248            learning_rates: Vec::new(),
249            epochs: Vec::new(),
250            validation_losses: Vec::new(),
251            training_times: Vec::new(),
252        }
253    }
254
255    pub fn record_epoch(&mut self, epoch: usize, loss: f64, lr: f64, training_time: f64) {
256        self.epochs.push(epoch);
257        self.losses.push(loss);
258        self.learning_rates.push(lr);
259        self.training_times.push(training_time);
260    }
261
262    pub fn record_validation(&mut self, val_loss: f64) {
263        self.validation_losses.push(val_loss);
264    }
265
266    pub fn get_smoothed_loss(&self, window_size: usize) -> Vec<f64> {
267        if self.losses.len() < window_size {
268            return self.losses.clone();
269        }
270
271        let mut smoothed = Vec::new();
272        let mut window: VecDeque<f64> = VecDeque::new();
273
274        for &loss in &self.losses {
275            window.push_back(loss);
276            if window.len() > window_size {
277                window.pop_front();
278            }
279
280            let avg = window.iter().sum::<f64>() / window.len() as f64;
281            smoothed.push(avg);
282        }
283
284        smoothed
285    }
286}
287
288impl Default for MetricsTracker {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294/// Advanced trainer with full optimization capabilities
295pub struct AdvancedTrainer {
296    config: TrainingConfig,
297    optimizer: OptimizerType,
298    scheduler: LearningRateScheduler,
299    early_stopping: Option<EarlyStopping>,
300    metrics: MetricsTracker,
301}
302
303impl AdvancedTrainer {
304    pub fn new(config: TrainingConfig) -> Self {
305        let early_stopping = if config.use_early_stopping {
306            Some(EarlyStopping::new(config.patience, config.min_delta))
307        } else {
308            None
309        };
310
311        Self {
312            config,
313            optimizer: OptimizerType::default(),
314            scheduler: LearningRateScheduler::default(),
315            early_stopping,
316            metrics: MetricsTracker::new(),
317        }
318    }
319
320    pub fn with_optimizer(mut self, optimizer: OptimizerType) -> Self {
321        self.optimizer = optimizer;
322        self
323    }
324
325    pub fn with_scheduler(mut self, scheduler: LearningRateScheduler) -> Self {
326        self.scheduler = scheduler;
327        self
328    }
329
330    pub async fn train(&mut self, model: &mut dyn EmbeddingModel) -> Result<TrainingStats> {
331        let start_time = Instant::now();
332        info!(
333            "Starting advanced training with {} epochs",
334            self.config.max_epochs
335        );
336
337        for epoch in 0..self.config.max_epochs {
338            let epoch_start = Instant::now();
339
340            // Get current learning rate
341            let current_lr = self
342                .scheduler
343                .get_lr(epoch, self.config.learning_rate, None);
344
345            // Train one epoch
346            let epoch_stats = model.train(Some(1)).await?;
347            let epoch_loss = epoch_stats.final_loss;
348            let epoch_time = epoch_start.elapsed().as_secs_f64();
349
350            // Record metrics
351            self.metrics
352                .record_epoch(epoch, epoch_loss, current_lr, epoch_time);
353
354            // Log progress
355            if epoch % self.config.log_freq == 0 {
356                debug!(
357                    "Epoch {}: loss = {:.6}, lr = {:.6}, time = {:.3}s",
358                    epoch, epoch_loss, current_lr, epoch_time
359                );
360            }
361
362            // Check early stopping
363            if let Some(ref mut early_stop) = self.early_stopping {
364                if early_stop.update(epoch_loss) {
365                    info!("Early stopping triggered at epoch {}", epoch);
366                    break;
367                }
368            }
369
370            // Simple convergence check
371            if epoch > 10 && epoch_loss < 1e-8 {
372                info!("Converged at epoch {} with loss {:.6}", epoch, epoch_loss);
373                break;
374            }
375        }
376
377        let training_time = start_time.elapsed().as_secs_f64();
378        let final_loss = self.metrics.losses.last().copied().unwrap_or(0.0);
379
380        Ok(TrainingStats {
381            epochs_completed: self.metrics.epochs.len(),
382            final_loss,
383            training_time_seconds: training_time,
384            convergence_achieved: final_loss < 1e-6,
385            loss_history: self.metrics.losses.clone(),
386        })
387    }
388
389    pub fn get_metrics(&self) -> &MetricsTracker {
390        &self.metrics
391    }
392}
393
394/// Validation utilities
395pub struct ValidationSuite {
396    pub test_triples: Vec<(String, String, String)>,
397    pub validation_freq: usize,
398}
399
400impl ValidationSuite {
401    pub fn new(test_triples: Vec<(String, String, String)>, validation_freq: usize) -> Self {
402        Self {
403            test_triples,
404            validation_freq,
405        }
406    }
407
408    pub fn evaluate_model(&self, model: &dyn EmbeddingModel) -> Result<ValidationMetrics> {
409        let mut total_score = 0.0;
410        let mut valid_predictions = 0;
411
412        for (subject, predicate, object) in &self.test_triples {
413            if let Ok(score) = model.score_triple(subject, predicate, object) {
414                total_score += score;
415                valid_predictions += 1;
416            }
417        }
418
419        let avg_score = if valid_predictions > 0 {
420            total_score / valid_predictions as f64
421        } else {
422            0.0
423        };
424
425        Ok(ValidationMetrics {
426            average_score: avg_score,
427            num_evaluated: valid_predictions,
428            num_total: self.test_triples.len(),
429        })
430    }
431}
432
433/// Validation metrics
434#[derive(Debug, Clone)]
435pub struct ValidationMetrics {
436    pub average_score: f64,
437    pub num_evaluated: usize,
438    pub num_total: usize,
439}
440
441/// Distributed training configuration
442#[derive(Debug, Clone)]
443pub struct DistributedConfig {
444    pub world_size: usize,
445    pub rank: usize,
446    pub device_ids: Vec<usize>,
447    pub backend: DistributedBackend,
448    pub sync_frequency: usize,
449    pub gradient_clipping: Option<f64>,
450    pub all_reduce_method: AllReduceMethod,
451}
452
453impl Default for DistributedConfig {
454    fn default() -> Self {
455        Self {
456            world_size: 1,
457            rank: 0,
458            device_ids: vec![0],
459            backend: DistributedBackend::NCCL,
460            sync_frequency: 1,
461            gradient_clipping: Some(1.0),
462            all_reduce_method: AllReduceMethod::Average,
463        }
464    }
465}
466
467/// Distributed backend options
468#[derive(Debug, Clone)]
469pub enum DistributedBackend {
470    NCCL,
471    MPI,
472    Gloo,
473}
474
475/// All-reduce methods for gradient synchronization
476#[derive(Debug, Clone)]
477pub enum AllReduceMethod {
478    Sum,
479    Average,
480    WeightedAverage,
481}
482
483/// Distributed trainer for multi-GPU/multi-node training
484#[allow(dead_code)]
485pub struct DistributedTrainer {
486    config: TrainingConfig,
487    distributed_config: DistributedConfig,
488    optimizer: OptimizerType,
489    scheduler: LearningRateScheduler,
490    early_stopping: Option<EarlyStopping>,
491    metrics: Arc<RwLock<MetricsTracker>>,
492    gradient_accumulator: Arc<Mutex<GradientAccumulator>>,
493    sync_channel: (
494        broadcast::Sender<SyncMessage>,
495        broadcast::Receiver<SyncMessage>,
496    ),
497}
498
499/// Messages for distributed synchronization
500#[derive(Debug, Clone)]
501pub enum SyncMessage {
502    GradientUpdate {
503        epoch: usize,
504        rank: usize,
505        gradients: Vec<f64>,
506    },
507    ParameterSync {
508        epoch: usize,
509        parameters: Vec<f64>,
510    },
511    EarlyStop {
512        epoch: usize,
513        loss: f64,
514    },
515    Checkpoint {
516        epoch: usize,
517        model_state: Vec<u8>,
518    },
519}
520
521/// Gradient accumulator for distributed training
522#[derive(Debug)]
523pub struct GradientAccumulator {
524    accumulated_gradients: Vec<Array2<f64>>,
525    accumulation_count: usize,
526    target_count: usize,
527}
528
529impl GradientAccumulator {
530    pub fn new(target_count: usize) -> Self {
531        Self {
532            accumulated_gradients: Vec::new(),
533            accumulation_count: 0,
534            target_count,
535        }
536    }
537
538    pub fn accumulate(&mut self, gradients: Vec<Array2<f64>>) {
539        if self.accumulated_gradients.is_empty() {
540            self.accumulated_gradients = gradients;
541        } else {
542            for (i, grad) in gradients.into_iter().enumerate() {
543                if i < self.accumulated_gradients.len() {
544                    self.accumulated_gradients[i] = &self.accumulated_gradients[i] + &grad;
545                } else {
546                    self.accumulated_gradients.push(grad);
547                }
548            }
549        }
550        self.accumulation_count += 1;
551    }
552
553    pub fn is_ready(&self) -> bool {
554        self.accumulation_count >= self.target_count
555    }
556
557    pub fn get_averaged_gradients(&mut self) -> Vec<Array2<f64>> {
558        let count = self.accumulation_count as f64;
559        let result = self
560            .accumulated_gradients
561            .iter()
562            .map(|grad| grad / count)
563            .collect();
564        self.reset();
565        result
566    }
567
568    pub fn reset(&mut self) {
569        self.accumulated_gradients.clear();
570        self.accumulation_count = 0;
571    }
572}
573
574impl DistributedTrainer {
575    pub fn new(config: TrainingConfig, distributed_config: DistributedConfig) -> Self {
576        let early_stopping = if config.use_early_stopping {
577            Some(EarlyStopping::new(config.patience, config.min_delta))
578        } else {
579            None
580        };
581
582        let (sync_tx, sync_rx) = broadcast::channel(1000);
583        let gradient_accumulator = Arc::new(Mutex::new(GradientAccumulator::new(
584            distributed_config.world_size,
585        )));
586
587        Self {
588            config,
589            distributed_config,
590            optimizer: OptimizerType::default(),
591            scheduler: LearningRateScheduler::default(),
592            early_stopping,
593            metrics: Arc::new(RwLock::new(MetricsTracker::new())),
594            gradient_accumulator,
595            sync_channel: (sync_tx, sync_rx),
596        }
597    }
598
599    pub fn with_optimizer(mut self, optimizer: OptimizerType) -> Self {
600        self.optimizer = optimizer;
601        self
602    }
603
604    pub fn with_scheduler(mut self, scheduler: LearningRateScheduler) -> Self {
605        self.scheduler = scheduler;
606        self
607    }
608
609    /// Run the multi-worker training loop over a shared model.
610    ///
611    /// Execution model (read before relying on this for scaling): every worker
612    /// operates on the *same* `Arc<RwLock<dyn EmbeddingModel>>` and replicates
613    /// the training loop, while the coordinator performs a **real synchronous
614    /// all-reduce** (sum-then-average across `world_size` ranks) of the per-rank
615    /// loss statistics exchanged each sync round and broadcasts the averaged
616    /// result back. Because the model is shared rather than sharded, this is a
617    /// training-loop-replication + loss-all-reduce harness, not full
618    /// parameter-level data-parallel SGD (no per-rank parameter gradients are
619    /// exchanged — there is a single shared parameter set). The all-reduce path
620    /// itself is genuinely executed, not simulated.
621    pub async fn train_distributed(
622        &mut self,
623        model: Arc<RwLock<dyn EmbeddingModel + Send + Sync>>,
624    ) -> Result<TrainingStats> {
625        let start_time = Instant::now();
626        info!(
627            "Starting distributed training with {} workers on rank {}",
628            self.distributed_config.world_size, self.distributed_config.rank
629        );
630
631        // Spawn worker tasks for each device
632        let mut worker_handles = Vec::new();
633
634        for device_id in &self.distributed_config.device_ids {
635            let worker_handle = self
636                .spawn_worker_task(*device_id, Arc::clone(&model))
637                .await?;
638            worker_handles.push(worker_handle);
639        }
640
641        // Spawn coordinator task
642        let coordinator_handle = self.spawn_coordinator_task().await?;
643
644        // Wait for all workers to complete
645        let mut final_stats = None;
646        for handle in worker_handles {
647            if let Ok(stats) = handle.await {
648                match stats {
649                    Ok(s) => final_stats = Some(s),
650                    Err(e) => warn!("Worker failed: {}", e),
651                }
652            }
653        }
654
655        // Stop coordinator
656        coordinator_handle.abort();
657
658        let training_time = start_time.elapsed().as_secs_f64();
659        let metrics = self.metrics.read().await;
660
661        Ok(final_stats.unwrap_or_else(|| TrainingStats {
662            epochs_completed: metrics.epochs.len(),
663            final_loss: metrics.losses.last().copied().unwrap_or(0.0),
664            training_time_seconds: training_time,
665            convergence_achieved: false,
666            loss_history: metrics.losses.clone(),
667        }))
668    }
669
670    /// Spawn a worker task for a specific device
671    async fn spawn_worker_task(
672        &self,
673        device_id: usize,
674        model: Arc<RwLock<dyn EmbeddingModel + Send + Sync>>,
675    ) -> Result<JoinHandle<Result<TrainingStats>>> {
676        let config = self.config.clone();
677        let distributed_config = self.distributed_config.clone();
678        let _optimizer = self.optimizer.clone();
679        let scheduler = self.scheduler.clone();
680        let metrics = Arc::clone(&self.metrics);
681        let mut sync_rx = self.sync_channel.0.subscribe();
682        let sync_tx = self.sync_channel.0.clone();
683
684        let handle = tokio::spawn(async move {
685            info!(
686                "Worker {} starting on device {}",
687                distributed_config.rank, device_id
688            );
689
690            let mut local_early_stopping = if config.use_early_stopping {
691                Some(EarlyStopping::new(config.patience, config.min_delta))
692            } else {
693                None
694            };
695
696            let mut total_training_time = 0.0;
697
698            for epoch in 0..config.max_epochs {
699                let epoch_start = Instant::now();
700
701                // Get current learning rate
702                let current_lr = scheduler.get_lr(epoch, config.learning_rate, None);
703
704                // Train one epoch on this device
705                let mut model_guard = model.write().await;
706                let epoch_stats = model_guard.train(Some(1)).await?;
707                drop(model_guard);
708
709                let epoch_loss = epoch_stats.final_loss;
710                let epoch_time = epoch_start.elapsed().as_secs_f64();
711                total_training_time += epoch_time;
712
713                // Record metrics
714                {
715                    let mut metrics_guard = metrics.write().await;
716                    metrics_guard.record_epoch(epoch, epoch_loss, current_lr, epoch_time);
717                }
718
719                // Synchronize per-rank loss statistics via the coordinator's
720                // real all-reduce.
721                if epoch % distributed_config.sync_frequency == 0 {
722                    // Contribute this rank's loss statistic to the all-reduce.
723                    let _ = sync_tx.send(SyncMessage::GradientUpdate {
724                        epoch,
725                        rank: distributed_config.rank,
726                        gradients: vec![epoch_loss],
727                    });
728
729                    // Wait for parameter updates
730                    tokio::select! {
731                        msg = sync_rx.recv() => {
732                            if let Ok(SyncMessage::ParameterSync { .. }) = msg {
733                                debug!("Received parameter sync for epoch {}", epoch);
734                            }
735                        }
736                        _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {
737                            debug!("Sync timeout for epoch {}", epoch);
738                        }
739                    }
740                }
741
742                // Log progress
743                if epoch % config.log_freq == 0 {
744                    debug!(
745                        "Worker {} Epoch {}: loss = {:.6}, lr = {:.6}, time = {:.3}s",
746                        distributed_config.rank, epoch, epoch_loss, current_lr, epoch_time
747                    );
748                }
749
750                // Check early stopping
751                if let Some(ref mut early_stop) = local_early_stopping {
752                    if early_stop.update(epoch_loss) {
753                        info!(
754                            "Worker {} early stopping triggered at epoch {}",
755                            distributed_config.rank, epoch
756                        );
757                        let _ = sync_tx.send(SyncMessage::EarlyStop {
758                            epoch,
759                            loss: epoch_loss,
760                        });
761                        break;
762                    }
763                }
764
765                // Simple convergence check
766                if epoch > 10 && epoch_loss < 1e-8 {
767                    info!(
768                        "Worker {} converged at epoch {} with loss {:.6}",
769                        distributed_config.rank, epoch, epoch_loss
770                    );
771                    break;
772                }
773            }
774
775            let final_metrics = metrics.read().await;
776            Ok(TrainingStats {
777                epochs_completed: final_metrics.epochs.len(),
778                final_loss: final_metrics.losses.last().copied().unwrap_or(0.0),
779                training_time_seconds: total_training_time,
780                convergence_achieved: final_metrics
781                    .losses
782                    .last()
783                    .copied()
784                    .unwrap_or(f64::INFINITY)
785                    < 1e-6,
786                loss_history: final_metrics.losses.clone(),
787            })
788        });
789
790        Ok(handle)
791    }
792
793    /// Spawn coordinator task for gradient synchronization
794    async fn spawn_coordinator_task(&self) -> Result<JoinHandle<()>> {
795        let mut sync_rx = self.sync_channel.0.subscribe();
796        let sync_tx = self.sync_channel.0.clone();
797        let gradient_accumulator = Arc::clone(&self.gradient_accumulator);
798        let world_size = self.distributed_config.world_size;
799
800        let handle = tokio::spawn(async move {
801            info!("Coordinator starting for {} workers", world_size);
802
803            // Per-epoch all-reduce buffers: element-wise running sum of each
804            // rank's contribution plus the number of ranks that have reported.
805            let mut pending: HashMap<usize, (Vec<f64>, usize)> = HashMap::new();
806
807            while let Ok(msg) = sync_rx.recv().await {
808                match msg {
809                    SyncMessage::GradientUpdate {
810                        epoch,
811                        rank,
812                        gradients,
813                    } => {
814                        debug!(
815                            "Received contribution from worker {} for epoch {}",
816                            rank, epoch
817                        );
818
819                        // Real all-reduce (sum stage): element-wise accumulate
820                        // this rank's vector into the epoch's running sum.
821                        let entry = pending
822                            .entry(epoch)
823                            .or_insert_with(|| (vec![0.0; gradients.len()], 0));
824                        if entry.0.len() < gradients.len() {
825                            entry.0.resize(gradients.len(), 0.0);
826                        }
827                        for (acc, g) in entry.0.iter_mut().zip(gradients.iter()) {
828                            *acc += *g;
829                        }
830                        entry.1 += 1;
831
832                        // Once every rank has reported, complete the all-reduce
833                        // (average stage) and broadcast the reduced result. The
834                        // shared GradientAccumulator is updated too so its state
835                        // reflects the real reduction round rather than a no-op.
836                        if entry.1 >= world_size {
837                            let (sum, count) = pending.remove(&epoch).unwrap_or((Vec::new(), 1));
838                            let divisor = count.max(1) as f64;
839                            let averaged: Vec<f64> = sum.iter().map(|v| v / divisor).collect();
840
841                            {
842                                let mut accumulator = gradient_accumulator
843                                    .lock()
844                                    .expect("lock should not be poisoned");
845                                let reduced_grad =
846                                    Array2::from_shape_vec((1, averaged.len()), averaged.clone())
847                                        .unwrap_or_else(|_| Array2::zeros((1, 0)));
848                                accumulator.accumulate(vec![reduced_grad]);
849                                if accumulator.is_ready() {
850                                    let _ = accumulator.get_averaged_gradients();
851                                }
852                            }
853
854                            let _ = sync_tx.send(SyncMessage::ParameterSync {
855                                epoch,
856                                parameters: averaged,
857                            });
858                        }
859                    }
860                    SyncMessage::EarlyStop { epoch, loss } => {
861                        info!(
862                            "Early stop signal received at epoch {} with loss {:.6}",
863                            epoch, loss
864                        );
865                        // Flush any partial reduction state for this epoch.
866                        pending.remove(&epoch);
867                    }
868                    _ => {}
869                }
870            }
871        });
872
873        Ok(handle)
874    }
875
876    /// Perform all-reduce operation on gradients
877    #[allow(dead_code)]
878    async fn all_reduce_gradients(&self, gradients: Vec<Array2<f64>>) -> Result<Vec<Array2<f64>>> {
879        // Simplified all-reduce - in practice would use NCCL/MPI
880        match self.distributed_config.all_reduce_method {
881            AllReduceMethod::Average => {
882                let world_size = self.distributed_config.world_size as f64;
883                Ok(gradients.into_iter().map(|g| g / world_size).collect())
884            }
885            AllReduceMethod::Sum => Ok(gradients),
886            AllReduceMethod::WeightedAverage => {
887                // Simplified - would use actual weights in practice
888                let world_size = self.distributed_config.world_size as f64;
889                Ok(gradients.into_iter().map(|g| g / world_size).collect())
890            }
891        }
892    }
893
894    /// Apply gradient clipping if configured
895    #[allow(dead_code)]
896    fn clip_gradients(&self, gradients: &mut [Array2<f64>]) {
897        if let Some(max_norm) = self.distributed_config.gradient_clipping {
898            for grad in gradients.iter_mut() {
899                let norm = grad.mapv(|x| x * x).sum().sqrt();
900                if norm > max_norm {
901                    *grad *= max_norm / norm;
902                }
903            }
904        }
905    }
906}
907
908/// Distributed training utilities
909pub struct DistributedUtils;
910
911impl DistributedUtils {
912    /// Initialize distributed training environment
913    pub async fn init_distributed(rank: usize, world_size: usize) -> Result<()> {
914        info!(
915            "Initializing distributed training: rank {} of {}",
916            rank, world_size
917        );
918        // In practice, would initialize NCCL/MPI here
919        Ok(())
920    }
921
922    /// Cleanup distributed training environment
923    pub async fn cleanup_distributed() -> Result<()> {
924        info!("Cleaning up distributed training environment");
925        // In practice, would cleanup NCCL/MPI here
926        Ok(())
927    }
928
929    /// Check if distributed training is available
930    pub fn is_distributed_available() -> bool {
931        // In practice, would check for NCCL/MPI availability
932        true
933    }
934
935    /// Get optimal world size for current hardware
936    pub fn get_optimal_world_size() -> usize {
937        // In practice, would detect available GPUs
938        std::thread::available_parallelism()
939            .map(|p| p.get())
940            .unwrap_or(1)
941    }
942}
943
944#[cfg(test)]
945mod tests {
946    use super::*;
947
948    #[test]
949    fn test_learning_rate_scheduler() {
950        let scheduler = LearningRateScheduler::ExponentialDecay {
951            decay_rate: 0.9,
952            decay_steps: 10,
953        };
954
955        let lr0 = scheduler.get_lr(0, 0.1, None);
956        let lr10 = scheduler.get_lr(10, 0.1, None);
957        let lr20 = scheduler.get_lr(20, 0.1, None);
958
959        assert!((lr0 - 0.1).abs() < 1e-10);
960        assert!(lr10 < lr0);
961        assert!(lr20 < lr10);
962    }
963
964    #[test]
965    fn test_early_stopping() {
966        let mut early_stop = EarlyStopping::new(3, 0.01);
967
968        assert!(!early_stop.update(1.0));
969        assert!(!early_stop.update(0.5));
970        assert!(!early_stop.update(0.51));
971        assert!(!early_stop.update(0.52));
972        assert!(!early_stop.update(0.53));
973        assert!(early_stop.update(0.54)); // Should stop now
974    }
975
976    #[test]
977    fn test_metrics_tracker() {
978        let mut tracker = MetricsTracker::new();
979
980        tracker.record_epoch(0, 1.0, 0.01, 1.5);
981        tracker.record_epoch(1, 0.5, 0.009, 1.4);
982        tracker.record_epoch(2, 0.3, 0.008, 1.3);
983
984        assert_eq!(tracker.losses.len(), 3);
985        assert_eq!(tracker.epochs.len(), 3);
986
987        let smoothed = tracker.get_smoothed_loss(2);
988        assert_eq!(smoothed.len(), 3);
989    }
990
991    #[test]
992    fn test_distributed_config() {
993        let config = DistributedConfig::default();
994        assert_eq!(config.world_size, 1);
995        assert_eq!(config.rank, 0);
996        assert_eq!(config.device_ids.len(), 1);
997    }
998
999    #[test]
1000    fn test_gradient_accumulator() {
1001        let mut accumulator = GradientAccumulator::new(2);
1002        assert!(!accumulator.is_ready());
1003
1004        let grad1 = vec![Array2::from_elem((2, 2), 1.0)];
1005        let grad2 = vec![Array2::from_elem((2, 2), 2.0)];
1006
1007        accumulator.accumulate(grad1);
1008        assert!(!accumulator.is_ready());
1009
1010        accumulator.accumulate(grad2);
1011        assert!(accumulator.is_ready());
1012
1013        let averaged = accumulator.get_averaged_gradients();
1014        assert_eq!(averaged.len(), 1);
1015        assert!((averaged[0][[0, 0]] - 1.5).abs() < 1e-10);
1016    }
1017
1018    #[test]
1019    fn test_distributed_utils() {
1020        assert!(DistributedUtils::is_distributed_available());
1021        let world_size = DistributedUtils::get_optimal_world_size();
1022        assert!(world_size >= 1);
1023    }
1024}