1use scirs2_core::gpu::{GpuBuffer, GpuContext, GpuDataType, GpuKernelHandle};
18use scirs2_core::ndarray::{ArrayBase, Data, DataMut, Dimension};
19use scirs2_core::numeric::Float;
20use std::marker::PhantomData;
21use std::sync::Arc;
22
23use crate::shaders::{CollectiveKernel, WORKGROUP_SIZE};
24use crate::GpuOptimError;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum SyncStrategy {
29 RingAllReduce,
31 TreeAllReduce,
33 HierarchicalAllReduce,
35 PipelineParallel,
37}
38
39#[derive(Debug, Clone)]
41pub struct MultiGpuConfig {
42 pub num_gpus: usize,
44 pub rank: usize,
46 pub sync_strategy: SyncStrategy,
48 pub gradient_compression: bool,
50 pub compression_ratio: f32,
52 pub local_group_size: usize,
54 pub adaptive_communication: bool,
56 pub bandwidth_monitor_interval: usize,
58 pub async_param_updates: bool,
60 pub communication_timeout_ms: u64,
62 pub error_correction: bool,
64 pub pipeline_depth: usize,
66}
67
68impl Default for MultiGpuConfig {
69 fn default() -> Self {
70 Self {
71 num_gpus: 1,
72 rank: 0,
73 sync_strategy: SyncStrategy::RingAllReduce,
74 gradient_compression: false,
75 compression_ratio: 0.1, local_group_size: 4,
77 adaptive_communication: true,
78 bandwidth_monitor_interval: 100,
79 async_param_updates: false,
80 communication_timeout_ms: 5000,
81 error_correction: true,
82 pipeline_depth: 2,
83 }
84 }
85}
86
87impl MultiGpuConfig {
88 pub fn validate(&self) -> Result<(), GpuOptimError> {
92 let invalid =
93 |what: &str| GpuOptimError::InvalidState(format!("invalid multi-GPU config: {what}"));
94 if self.num_gpus == 0 {
95 return Err(invalid("num_gpus must be >= 1"));
96 }
97 if self.rank >= self.num_gpus {
98 return Err(invalid("rank must be < num_gpus"));
99 }
100 if self.local_group_size == 0 {
101 return Err(invalid("local_group_size must be >= 1"));
102 }
103 if self.pipeline_depth == 0 {
104 return Err(invalid("pipeline_depth must be >= 1"));
105 }
106 if self.gradient_compression
107 && !(self.compression_ratio.is_finite()
108 && self.compression_ratio > 0.0
109 && self.compression_ratio <= 1.0)
110 {
111 return Err(invalid("compression_ratio must be finite and in (0, 1]"));
112 }
113 Ok(())
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct CommunicationPerformanceMonitor {
120 total_comm_time_us: u64,
122 total_data_bytes: u64,
124 comm_operations: usize,
126 bandwidth_history: std::collections::VecDeque<f64>,
128 strategy_performance: std::collections::HashMap<SyncStrategy, StrategyPerformanceMetrics>,
130}
131
132impl CommunicationPerformanceMonitor {
133 fn new() -> Self {
134 Self {
135 total_comm_time_us: 0,
136 total_data_bytes: 0,
137 comm_operations: 0,
138 bandwidth_history: std::collections::VecDeque::with_capacity(1000),
139 strategy_performance: std::collections::HashMap::new(),
140 }
141 }
142
143 fn record_communication(
144 &mut self,
145 strategy: SyncStrategy,
146 data_bytes: u64,
147 timeus: u64,
148 tensor_size: usize,
149 ) {
150 let timeus = timeus.max(1);
154 self.total_comm_time_us += timeus;
155 self.total_data_bytes += data_bytes;
156 self.comm_operations += 1;
157
158 let bandwidth_gb_s = (data_bytes as f64) / (timeus as f64 / 1_000_000.0) / 1e9;
159 self.bandwidth_history.push_back(bandwidth_gb_s);
160
161 if self.bandwidth_history.len() > 1000 {
162 self.bandwidth_history.pop_front();
163 }
164
165 let metrics = self
167 .strategy_performance
168 .entry(strategy)
169 .or_insert_with(StrategyPerformanceMetrics::new);
170 metrics.update(bandwidth_gb_s, timeus, tensor_size);
171 }
172
173 fn get_average_bandwidth(&self) -> f64 {
174 if self.total_comm_time_us == 0 {
175 0.0
176 } else {
177 (self.total_data_bytes as f64) / (self.total_comm_time_us as f64 / 1_000_000.0) / 1e9
178 }
179 }
180
181 fn get_optimal_strategy(&self, tensorsize: usize) -> SyncStrategy {
182 let mut best_strategy = SyncStrategy::RingAllReduce;
183 let mut best_score = 0.0;
184
185 for (strategy, metrics) in &self.strategy_performance {
186 let score = metrics.calculate_score(tensorsize);
187 if score > best_score {
188 best_score = score;
189 best_strategy = *strategy;
190 }
191 }
192
193 best_strategy
194 }
195}
196
197#[derive(Debug, Clone)]
199struct StrategyPerformanceMetrics {
200 bandwidth_samples: std::collections::VecDeque<f64>,
201 latency_samples: std::collections::VecDeque<u64>,
202 tensor_sizes: std::collections::VecDeque<usize>,
203 efficiency_score: f64,
204}
205
206impl StrategyPerformanceMetrics {
207 fn new() -> Self {
208 Self {
209 bandwidth_samples: std::collections::VecDeque::with_capacity(100),
210 latency_samples: std::collections::VecDeque::with_capacity(100),
211 tensor_sizes: std::collections::VecDeque::with_capacity(100),
212 efficiency_score: 0.0,
213 }
214 }
215
216 fn update(&mut self, bandwidth_gb_s: f64, latencyus: u64, tensor_size: usize) {
217 self.bandwidth_samples.push_back(bandwidth_gb_s);
218 self.latency_samples.push_back(latencyus);
219 self.tensor_sizes.push_back(tensor_size);
220
221 if self.bandwidth_samples.len() > 100 {
222 self.bandwidth_samples.pop_front();
223 self.latency_samples.pop_front();
224 self.tensor_sizes.pop_front();
225 }
226
227 let avg_bandwidth =
229 self.bandwidth_samples.iter().sum::<f64>() / self.bandwidth_samples.len() as f64;
230 let avg_latency =
231 self.latency_samples.iter().sum::<u64>() as f64 / self.latency_samples.len() as f64;
232
233 self.efficiency_score = avg_bandwidth / (avg_latency / 1000.0); }
235
236 fn calculate_score(&self, tensorsize: usize) -> f64 {
237 let size_factor = if tensorsize > 1000000 { 2.0 } else { 1.0 }; let has_comparable_history = self.tensor_sizes.is_empty()
247 || self.tensor_sizes.iter().any(|&recorded| {
248 let (small, large) = if recorded <= tensorsize {
249 (recorded.max(1), tensorsize.max(1))
250 } else {
251 (tensorsize.max(1), recorded)
252 };
253 large <= small * 10
254 });
255 let relevance = if has_comparable_history { 1.0 } else { 0.5 };
256
257 self.efficiency_score * size_factor * relevance
258 }
259}
260
261#[derive(Debug)]
263pub struct AdaptiveCommunicationSelector {
264 current_strategy: SyncStrategy,
266 switch_cooldown: usize,
268 last_switch_step: usize,
270 #[allow(dead_code)]
285 evaluation_window: usize,
286 performance_threshold: f64,
288}
289
290impl AdaptiveCommunicationSelector {
291 fn new() -> Self {
292 Self {
293 current_strategy: SyncStrategy::RingAllReduce,
294 switch_cooldown: 50,
295 last_switch_step: 0,
296 evaluation_window: 20,
297 performance_threshold: 1.2, }
299 }
300
301 fn should_evaluate_strategy(&self, currentstep: usize) -> bool {
302 currentstep - self.last_switch_step >= self.switch_cooldown
303 }
304
305 fn evaluate_and_switch(
306 &mut self,
307 monitor: &CommunicationPerformanceMonitor,
308 tensor_size: usize,
309 current_step: usize,
310 ) -> Option<SyncStrategy> {
311 if !self.should_evaluate_strategy(current_step) {
312 return None;
313 }
314
315 let optimal_strategy = monitor.get_optimal_strategy(tensor_size);
316
317 if optimal_strategy != self.current_strategy {
318 if let (Some(current_metrics), Some(optimal_metrics)) = (
320 monitor.strategy_performance.get(&self.current_strategy),
321 monitor.strategy_performance.get(&optimal_strategy),
322 ) {
323 let performance_ratio =
324 optimal_metrics.efficiency_score / current_metrics.efficiency_score;
325
326 if performance_ratio >= self.performance_threshold {
327 self.current_strategy = optimal_strategy;
328 self.last_switch_step = current_step;
329 return Some(optimal_strategy);
330 }
331 }
332 }
333
334 None
335 }
336}
337
338#[derive(Debug, Clone)]
340pub struct CommunicationPerformanceStats {
341 pub average_bandwidth_gb_s: f64,
342 pub total_operations: usize,
343 pub total_data_transferred_gb: f64,
344 pub current_strategy: SyncStrategy,
345 pub pending_async_ops: usize,
349 pub step_count: usize,
350}
351
352fn encode_u32(value: usize) -> Result<f32, GpuOptimError> {
355 let raw = u32::try_from(value).map_err(|_| {
356 GpuOptimError::UnsupportedOperation(format!("{value} does not fit in a u32 kernel operand"))
357 })?;
358 Ok(f32::from_bits(raw))
359}
360
361fn workgroup_count(n: usize) -> Result<u32, GpuOptimError> {
363 let groups = n.div_ceil(WORKGROUP_SIZE);
364 u32::try_from(groups).map_err(|_| {
365 GpuOptimError::UnsupportedOperation(format!(
366 "{n} elements need {groups} workgroups, which exceeds the u32 dispatch limit"
367 ))
368 })
369}
370
371fn dispatch_local_reduce(
379 context: &GpuContext,
380 kernel: &GpuKernelHandle,
381 host: &[f32],
382 num_gpus: usize,
383) -> Result<Vec<f32>, GpuOptimError> {
384 let n = host.len();
385 let hyper = [encode_u32(n)?, encode_u32(num_gpus)?];
386 let groups = workgroup_count(n)?;
387
388 let x_buf = context.create_buffer::<f32>(n);
389 x_buf.copy_from_host(host)?;
390 let y_buf = context.create_buffer::<f32>(hyper.len());
391 y_buf.copy_from_host(&hyper)?;
392
393 kernel.set_buffer("x", &x_buf);
394 kernel.set_buffer("y", &y_buf);
395 kernel.dispatch([groups, 1, 1]);
396
397 let mut out = vec![0.0f32; n];
398 x_buf.copy_to_host(&mut out)?;
399 Ok(out)
400}
401
402pub struct MultiGpuSync<A: Float + GpuDataType> {
404 context: Arc<GpuContext>,
406 config: MultiGpuConfig,
408 max_param_size: usize,
410 reduce_kernel: Option<GpuKernelHandle>,
416 perf_monitor: CommunicationPerformanceMonitor,
418 adaptive_selector: AdaptiveCommunicationSelector,
420 step_counter: usize,
422 _phantom: PhantomData<A>,
424}
425
426impl<A: Float + GpuDataType + Send + Sync> MultiGpuSync<A> {
427 pub fn new(
433 context: Arc<GpuContext>,
434 config: MultiGpuConfig,
435 max_param_size: usize,
436 ) -> Result<Self, GpuOptimError> {
437 config.validate()?;
438
439 let reduce_kernel = match CollectiveKernel::AllReduceMean.source_for(context.backend()) {
440 Some(source) => Some(context.execute(|compiler| compiler.compile(source))?),
441 None => None,
442 };
443
444 Ok(Self {
445 context,
446 config,
447 max_param_size,
448 reduce_kernel,
449 perf_monitor: CommunicationPerformanceMonitor::new(),
450 adaptive_selector: AdaptiveCommunicationSelector::new(),
451 step_counter: 0,
452 _phantom: PhantomData,
453 })
454 }
455
456 pub fn sync_gradients<S, D>(
458 &mut self,
459 gradients: &mut ArrayBase<S, D>,
460 ) -> Result<(), GpuOptimError>
461 where
462 S: DataMut<Elem = A>,
463 D: Dimension,
464 {
465 self.step_counter += 1;
466 let tensor_size = gradients.len();
467 let start_time = std::time::Instant::now();
468
469 let strategy = if self.config.adaptive_communication {
471 if let Some(new_strategy) = self.adaptive_selector.evaluate_and_switch(
472 &self.perf_monitor,
473 tensor_size,
474 self.step_counter,
475 ) {
476 new_strategy
477 } else {
478 self.adaptive_selector.current_strategy
479 }
480 } else {
481 self.config.sync_strategy
482 };
483
484 let result = match strategy {
489 SyncStrategy::RingAllReduce
490 | SyncStrategy::TreeAllReduce
491 | SyncStrategy::HierarchicalAllReduce => self.local_reduce(gradients),
492 SyncStrategy::PipelineParallel => {
493 if self.config.async_param_updates {
494 self.pipeline_parallel_async(gradients)
495 } else {
496 Err(GpuOptimError::UnsupportedOperation(
497 "Pipeline parallel requires async updates enabled".to_string(),
498 ))
499 }
500 }
501 };
502
503 let elapsed = start_time.elapsed();
505 let data_bytes = tensor_size * std::mem::size_of::<A>();
506
507 self.perf_monitor.record_communication(
508 strategy,
509 data_bytes as u64,
510 elapsed.as_micros() as u64,
511 tensor_size,
512 );
513
514 if self
516 .step_counter
517 .is_multiple_of(self.config.bandwidth_monitor_interval)
518 {
519 self.log_performance_statistics();
520 }
521
522 result
523 }
524
525 fn local_reduce<S, D>(&self, gradients: &mut ArrayBase<S, D>) -> Result<(), GpuOptimError>
532 where
533 S: DataMut<Elem = A>,
534 D: Dimension,
535 {
536 if self.config.num_gpus > 1 {
537 return Err(GpuOptimError::UnsupportedOperation(format!(
538 "all-reduce across {} GPUs needs a cross-device transport (an NCCL/MPI \
539 equivalent); this build has a single scirs2_core::gpu::GpuContext and no such \
540 transport, so peer devices' data can never be fetched",
541 self.config.num_gpus
542 )));
543 }
544 let n = gradients.len();
545 if n == 0 {
546 return Ok(());
547 }
548 if n > self.max_param_size {
549 return Err(GpuOptimError::InvalidState(format!(
550 "gradient tensor has {n} elements, above the {}-element bound this \
551 MultiGpuSync was constructed with",
552 self.max_param_size
553 )));
554 }
555 let kernel = self.reduce_kernel.as_ref().ok_or_else(|| {
556 GpuOptimError::UnsupportedOperation(format!(
557 "no all-reduce kernel source for backend {}",
558 self.context.backend()
559 ))
560 })?;
561
562 let host: Vec<f32> = gradients
563 .iter()
564 .map(|v| v.to_f32().unwrap_or(0.0))
565 .collect();
566 let out = dispatch_local_reduce(&self.context, kernel, &host, 1)?;
567 write_back(gradients, &out)
568 }
569
570 fn pipeline_parallel_async<S, D>(
580 &mut self,
581 gradients: &mut ArrayBase<S, D>,
582 ) -> Result<(), GpuOptimError>
583 where
584 S: DataMut<Elem = A>,
585 D: Dimension,
586 {
587 if self.config.num_gpus > 1 {
588 return Err(GpuOptimError::UnsupportedOperation(format!(
589 "pipeline-parallel sync across {} GPUs needs a cross-device transport this \
590 build does not have",
591 self.config.num_gpus
592 )));
593 }
594 let n = gradients.len();
595 if n == 0 {
596 return Ok(());
597 }
598 if n > self.max_param_size {
599 return Err(GpuOptimError::InvalidState(format!(
600 "gradient tensor has {n} elements, above the {}-element bound this \
601 MultiGpuSync was constructed with",
602 self.max_param_size
603 )));
604 }
605 let kernel = self.reduce_kernel.as_ref().ok_or_else(|| {
606 GpuOptimError::UnsupportedOperation(format!(
607 "no all-reduce kernel source for backend {}",
608 self.context.backend()
609 ))
610 })?;
611
612 let host: Vec<f32> = gradients
613 .iter()
614 .map(|v| v.to_f32().unwrap_or(0.0))
615 .collect();
616 let depth = self.config.pipeline_depth.max(1);
617 let chunk_size = n.div_ceil(depth).max(1);
620
621 let mut chunks: Vec<(usize, usize, GpuBuffer<f32>)> = Vec::with_capacity(depth);
622 for stage in 0..depth {
623 let start = stage * chunk_size;
624 if start >= n {
625 break;
626 }
627 let end = (start + chunk_size).min(n);
628 let hyper = [encode_u32(end - start)?, encode_u32(1)?];
629
630 let x_buf = self.context.create_buffer::<f32>(end - start);
631 x_buf.copy_from_host(&host[start..end])?;
632 let y_buf = self.context.create_buffer::<f32>(hyper.len());
633 y_buf.copy_from_host(&hyper)?;
634
635 kernel.set_buffer("x", &x_buf);
636 kernel.set_buffer("y", &y_buf);
637 kernel.dispatch_no_wait([workgroup_count(end - start)?, 1, 1]);
638 chunks.push((start, end, x_buf));
639 }
640
641 self.context.gpu_sync()?;
645
646 let mut out = vec![0.0f32; n];
647 for (start, end, buf) in &chunks {
648 buf.copy_to_host(&mut out[*start..*end])?;
649 }
650
651 write_back(gradients, &out)
652 }
653
654 fn log_performance_statistics(&self) {
656 let avg_bandwidth = self.perf_monitor.get_average_bandwidth();
657 let total_ops = self.perf_monitor.comm_operations;
658
659 log::info!(
660 "Multi-GPU Performance [Step {}]: {:.2} GB/s avg bandwidth, {} ops, current strategy: {:?}",
661 self.step_counter,
662 avg_bandwidth,
663 total_ops,
664 self.adaptive_selector.current_strategy
665 );
666 }
667
668 pub fn get_performance_stats(&self) -> CommunicationPerformanceStats {
670 CommunicationPerformanceStats {
671 average_bandwidth_gb_s: self.perf_monitor.get_average_bandwidth(),
672 total_operations: self.perf_monitor.comm_operations,
673 total_data_transferred_gb: self.perf_monitor.total_data_bytes as f64 / 1e9,
674 current_strategy: self.adaptive_selector.current_strategy,
675 pending_async_ops: 0,
676 step_count: self.step_counter,
677 }
678 }
679
680 pub fn synchronize_all(&mut self) -> Result<(), GpuOptimError> {
682 self.context.gpu_sync().map_err(GpuOptimError::from)
683 }
684
685 pub fn compress_gradients<S, D>(
693 &mut self,
694 gradients: &ArrayBase<S, D>,
695 ) -> Result<(Vec<A>, Vec<i32>), GpuOptimError>
696 where
697 S: Data<Elem = A>,
698 D: Dimension,
699 {
700 let len = gradients.len();
701 if len == 0 {
702 return Ok((Vec::new(), Vec::new()));
703 }
704 let k = (((len as f64) * (self.config.compression_ratio as f64)).round() as usize)
708 .clamp(1, len);
709
710 let mut indexed: Vec<(usize, A)> = gradients.iter().copied().enumerate().collect();
711 indexed.sort_by(|(_, a), (_, b)| {
714 b.abs()
715 .partial_cmp(&a.abs())
716 .unwrap_or(std::cmp::Ordering::Equal)
717 });
718 indexed.truncate(k);
719
720 let mut values = Vec::with_capacity(k);
721 let mut indices = Vec::with_capacity(k);
722 for (idx, value) in indexed {
723 values.push(value);
724 indices.push(idx as i32);
725 }
726 Ok((values, indices))
727 }
728}
729
730fn write_back<A, S, D>(array: &mut ArrayBase<S, D>, values: &[f32]) -> Result<(), GpuOptimError>
734where
735 A: Float,
736 S: DataMut<Elem = A>,
737 D: Dimension,
738{
739 for (dst, &src) in array.iter_mut().zip(values.iter()) {
740 *dst = A::from(src).ok_or_else(|| {
741 GpuOptimError::InvalidState(format!(
742 "{src} is not representable in the target float type"
743 ))
744 })?;
745 }
746 Ok(())
747}
748
749pub struct MultiGpuSetup {
751 pub contexts: Vec<Arc<GpuContext>>,
753 pub sync_managers: Vec<MultiGpuSync<f32>>,
755}
756
757impl MultiGpuSetup {
758 pub fn new(num_gpus: usize, max_param_size: usize) -> Result<Self, GpuOptimError> {
767 let mut reasons = Vec::new();
768 let mut opened = None;
769 for backend in crate::optimizers::SUPPORTED_BACKENDS {
770 match GpuContext::new(backend) {
771 Ok(context) => {
772 opened = Some(context);
773 break;
774 }
775 Err(e) => reasons.push(format!("{backend}: {e}")),
776 }
777 }
778 let Some(shared_context) = opened else {
779 return Err(GpuOptimError::UnsupportedOperation(format!(
780 "no GPU backend available for multi-GPU setup ({})",
781 reasons.join("; ")
782 )));
783 };
784
785 let mut contexts = Vec::with_capacity(num_gpus);
786 let mut sync_managers = Vec::with_capacity(num_gpus);
787 let context = Arc::new(shared_context);
788
789 for rank in 0..num_gpus {
790 let config = MultiGpuConfig {
791 num_gpus,
792 rank,
793 ..Default::default()
794 };
795
796 let sync_manager = MultiGpuSync::new(context.clone(), config, max_param_size)?;
797
798 contexts.push(context.clone());
799 sync_managers.push(sync_manager);
800 }
801
802 Ok(Self {
803 contexts,
804 sync_managers,
805 })
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::optimizers::SUPPORTED_BACKENDS;
813 use scirs2_core::ndarray::Array1;
814
815 #[test]
816 fn test_multi_gpu_config_default() {
817 let config = MultiGpuConfig::default();
818 assert_eq!(config.num_gpus, 1);
819 assert_eq!(config.rank, 0);
820 assert_eq!(config.sync_strategy, SyncStrategy::RingAllReduce);
821 assert!(!config.gradient_compression);
822 assert!(config.validate().is_ok());
823 }
824
825 #[test]
826 fn config_validate_rejects_divide_by_zero_fields() {
827 let base = MultiGpuConfig::default();
828 assert!(MultiGpuConfig {
829 num_gpus: 0,
830 ..base.clone()
831 }
832 .validate()
833 .is_err());
834 assert!(MultiGpuConfig {
835 local_group_size: 0,
836 ..base.clone()
837 }
838 .validate()
839 .is_err());
840 assert!(MultiGpuConfig {
841 pipeline_depth: 0,
842 ..base.clone()
843 }
844 .validate()
845 .is_err());
846 assert!(MultiGpuConfig {
847 rank: 5,
848 num_gpus: 2,
849 ..base.clone()
850 }
851 .validate()
852 .is_err());
853 assert!(MultiGpuConfig {
854 gradient_compression: true,
855 compression_ratio: 0.0,
856 ..base.clone()
857 }
858 .validate()
859 .is_err());
860 assert!(MultiGpuConfig {
861 gradient_compression: true,
862 compression_ratio: f32::NAN,
863 ..base
864 }
865 .validate()
866 .is_err());
867 }
868
869 #[test]
870 fn test_sync_strategy_selection() {
871 let strategies = [
872 SyncStrategy::RingAllReduce,
873 SyncStrategy::TreeAllReduce,
874 SyncStrategy::HierarchicalAllReduce,
875 SyncStrategy::PipelineParallel,
876 ];
877
878 for strategy in &strategies {
879 let config = MultiGpuConfig {
880 sync_strategy: *strategy,
881 ..Default::default()
882 };
883 assert_eq!(config.sync_strategy, *strategy);
884 }
885 }
886
887 #[test]
888 fn test_communication_performance_monitor() {
889 let mut monitor = CommunicationPerformanceMonitor::new();
890
891 monitor.record_communication(SyncStrategy::RingAllReduce, 1000000, 1000, 1000000); monitor.record_communication(SyncStrategy::TreeAllReduce, 2000000, 1000, 1000000); assert_eq!(monitor.comm_operations, 2);
896 assert!(monitor.get_average_bandwidth() > 0.0);
897
898 let optimal = monitor.get_optimal_strategy(1000000);
900 assert!(matches!(
901 optimal,
902 SyncStrategy::RingAllReduce | SyncStrategy::TreeAllReduce
903 ));
904 }
905
906 #[test]
909 fn record_communication_clamps_zero_elapsed_time() {
910 let mut monitor = CommunicationPerformanceMonitor::new();
911 monitor.record_communication(SyncStrategy::RingAllReduce, 1_000_000, 0, 1_000_000);
912 let avg = monitor.get_average_bandwidth();
913 assert!(avg.is_finite(), "average bandwidth was not finite: {avg}");
914 assert!(avg > 0.0);
915 assert!(monitor
916 .bandwidth_history
917 .back()
918 .copied()
919 .unwrap_or(f64::NAN)
920 .is_finite());
921 }
922
923 #[test]
924 fn test_adaptive_communication_selector() {
925 let mut selector = AdaptiveCommunicationSelector::new();
926 let mut monitor = CommunicationPerformanceMonitor::new();
927
928 assert_eq!(selector.current_strategy, SyncStrategy::RingAllReduce);
930
931 for _ in 0..10 {
933 monitor.record_communication(SyncStrategy::TreeAllReduce, 1000000, 500, 1000000);
934 }
936
937 let new_strategy = selector.evaluate_and_switch(&monitor, 1000000, 100);
939
940 if let Some(strategy) = new_strategy {
942 assert_ne!(strategy, SyncStrategy::RingAllReduce);
943 }
944 }
945
946 #[test]
947 fn test_multi_gpu_config_extended() {
948 let config = MultiGpuConfig {
949 num_gpus: 8,
950 adaptive_communication: true,
951 bandwidth_monitor_interval: 50,
952 async_param_updates: true,
953 communication_timeout_ms: 1000,
954 error_correction: true,
955 pipeline_depth: 4,
956 ..Default::default()
957 };
958
959 assert_eq!(config.num_gpus, 8);
960 assert!(config.adaptive_communication);
961 assert_eq!(config.bandwidth_monitor_interval, 50);
962 assert!(config.async_param_updates);
963 assert_eq!(config.communication_timeout_ms, 1000);
964 assert!(config.error_correction);
965 assert_eq!(config.pipeline_depth, 4);
966 }
967
968 #[test]
969 fn test_strategy_performance_metrics() {
970 let mut metrics = StrategyPerformanceMetrics::new();
971
972 metrics.update(10.0, 1000, 1000000); metrics.update(15.0, 800, 1000000); assert!(metrics.efficiency_score > 0.0);
976
977 let score = metrics.calculate_score(1000000); assert!(score > 0.0);
979 }
980
981 #[test]
987 fn test_calculate_score_discounts_unfamiliar_tensor_sizes() {
988 let mut metrics = StrategyPerformanceMetrics::new();
989 metrics.update(10.0, 1000, 1_000_000);
990 metrics.update(10.0, 1000, 1_000_000);
991
992 let familiar = metrics.calculate_score(1_000_000);
993 let unfamiliar = metrics.calculate_score(1_000);
994
995 assert!(
996 unfamiliar < familiar,
997 "score for an unfamiliar tensor size ({unfamiliar}) should be lower than for a \
998 size this strategy has a track record at ({familiar})"
999 );
1000 }
1001
1002 #[test]
1003 fn test_communication_performance_stats() {
1004 let stats = CommunicationPerformanceStats {
1005 average_bandwidth_gb_s: 10.5,
1006 total_operations: 100,
1007 total_data_transferred_gb: 50.0,
1008 current_strategy: SyncStrategy::RingAllReduce,
1009 pending_async_ops: 0,
1010 step_count: 1000,
1011 };
1012
1013 assert_eq!(stats.average_bandwidth_gb_s, 10.5);
1014 assert_eq!(stats.total_operations, 100);
1015 assert_eq!(stats.total_data_transferred_gb, 50.0);
1016 assert_eq!(stats.current_strategy, SyncStrategy::RingAllReduce);
1017 assert_eq!(stats.pending_async_ops, 0);
1018 assert_eq!(stats.step_count, 1000);
1019 }
1020
1021 #[test]
1025 fn compress_gradients_selects_real_top_k() {
1026 let context = match probe_backend() {
1027 Some(backend) => Arc::new(GpuContext::new(backend).expect("backend just probed")),
1028 None => {
1029 eprintln!("SKIP: compress_gradients_selects_real_top_k — no usable GPU backend");
1030 return;
1031 }
1032 };
1033 let config = MultiGpuConfig {
1034 gradient_compression: true,
1035 compression_ratio: 0.25,
1036 ..Default::default()
1037 };
1038 let mut sync = MultiGpuSync::<f32>::new(context, config, 1024).expect("construction");
1039
1040 let data = Array1::from(vec![0.1f32, -5.0, 2.0, 0.3, -4.0, 1.0, 0.05, -0.2]);
1041 let (values, indices) = sync.compress_gradients(&data).expect("compression");
1042
1043 assert_eq!(values.len(), 2);
1046 assert_eq!(indices.len(), 2);
1047 let mut got: Vec<(i32, f32)> = indices.into_iter().zip(values).collect();
1048 got.sort_by_key(|(idx, _)| *idx);
1049 assert_eq!(got, vec![(1, -5.0), (4, -4.0)]);
1050 }
1051
1052 #[test]
1053 fn compress_gradients_ratio_never_selects_zero_elements() {
1054 let context = match probe_backend() {
1055 Some(backend) => Arc::new(GpuContext::new(backend).expect("backend just probed")),
1056 None => {
1057 eprintln!(
1058 "SKIP: compress_gradients_ratio_never_selects_zero_elements — no usable GPU backend"
1059 );
1060 return;
1061 }
1062 };
1063 let config = MultiGpuConfig {
1064 gradient_compression: true,
1065 compression_ratio: 0.01, ..Default::default()
1067 };
1068 let mut sync = MultiGpuSync::<f32>::new(context, config, 1024).expect("construction");
1069 let data = Array1::from(vec![1.0f32, 2.0, 3.0, 4.0]);
1070 let (values, _) = sync.compress_gradients(&data).expect("compression");
1071 assert_eq!(
1072 values.len(),
1073 1,
1074 "a nonzero ratio must keep at least one element"
1075 );
1076 }
1077
1078 fn probe_backend() -> Option<scirs2_core::gpu::GpuBackend> {
1079 SUPPORTED_BACKENDS
1080 .into_iter()
1081 .find(|&backend| GpuContext::new(backend).is_ok())
1082 }
1083
1084 #[test]
1089 fn single_device_sync_runs_a_real_kernel_and_is_the_identity() {
1090 let backend = match probe_backend() {
1091 Some(b) => b,
1092 None => {
1093 eprintln!(
1094 "SKIP: single_device_sync_runs_a_real_kernel_and_is_the_identity — no usable GPU backend"
1095 );
1096 return;
1097 }
1098 };
1099 let context = Arc::new(GpuContext::new(backend).expect("backend just probed"));
1100 let config = MultiGpuConfig::default(); let mut sync = MultiGpuSync::<f32>::new(context, config, 4096).expect("construction");
1102
1103 for strategy in [
1104 SyncStrategy::RingAllReduce,
1105 SyncStrategy::TreeAllReduce,
1106 SyncStrategy::HierarchicalAllReduce,
1107 ] {
1108 sync.config.sync_strategy = strategy;
1109 let original: Array1<f32> =
1110 Array1::from((0..777).map(|i| i as f32 * 0.5 - 10.0).collect::<Vec<_>>());
1111 let mut grads = original.clone();
1112 sync.sync_gradients(&mut grads).unwrap_or_else(|e| {
1113 panic!("{strategy:?}: single-device sync must succeed, got {e}")
1114 });
1115 for (a, b) in original.iter().zip(grads.iter()) {
1116 assert!(
1117 (a - b).abs() < 1e-5,
1118 "{strategy:?}: single-device all-reduce changed the data: {a} -> {b}"
1119 );
1120 }
1121 }
1122 }
1123
1124 #[test]
1127 fn multi_device_sync_is_an_honest_unsupported_error() {
1128 let backend = match probe_backend() {
1129 Some(b) => b,
1130 None => {
1131 eprintln!("SKIP: multi_device_sync_is_an_honest_unsupported_error — no usable GPU backend");
1132 return;
1133 }
1134 };
1135 let context = Arc::new(GpuContext::new(backend).expect("backend just probed"));
1136 let config = MultiGpuConfig {
1137 num_gpus: 2,
1138 ..Default::default()
1139 };
1140 let mut sync = MultiGpuSync::<f32>::new(context, config, 4096).expect("construction");
1141 let mut grads = Array1::from_elem(16, 1.0f32);
1142 let err = sync
1143 .sync_gradients(&mut grads)
1144 .expect_err("num_gpus > 1 must fail, not silently succeed");
1145 assert!(matches!(err, GpuOptimError::UnsupportedOperation(_)));
1146 }
1147
1148 #[test]
1151 fn pipeline_parallel_covers_every_element_including_the_tail() {
1152 let backend = match probe_backend() {
1153 Some(b) => b,
1154 None => {
1155 eprintln!(
1156 "SKIP: pipeline_parallel_covers_every_element_including_the_tail — no usable GPU backend"
1157 );
1158 return;
1159 }
1160 };
1161 let context = Arc::new(GpuContext::new(backend).expect("backend just probed"));
1162 let config = MultiGpuConfig {
1163 sync_strategy: SyncStrategy::PipelineParallel,
1164 async_param_updates: true,
1165 pipeline_depth: 4,
1166 adaptive_communication: false,
1167 ..Default::default()
1168 };
1169 let mut sync = MultiGpuSync::<f32>::new(context, config, 4096).expect("construction");
1170
1171 let original: Array1<f32> = Array1::from((0..777).map(|i| i as f32).collect::<Vec<_>>());
1173 let mut grads = original.clone();
1174 sync.sync_gradients(&mut grads).expect("pipeline sync");
1175 for (i, (a, b)) in original.iter().zip(grads.iter()).enumerate() {
1176 assert!(
1177 (a - b).abs() < 1e-5,
1178 "element {i} was dropped or corrupted: {a} -> {b}"
1179 );
1180 }
1181 }
1182
1183 #[test]
1184 fn synchronize_all_waits_on_a_real_fence() {
1185 let backend = match probe_backend() {
1186 Some(b) => b,
1187 None => {
1188 eprintln!("SKIP: synchronize_all_waits_on_a_real_fence — no usable GPU backend");
1189 return;
1190 }
1191 };
1192 let context = Arc::new(GpuContext::new(backend).expect("backend just probed"));
1193 let mut sync = MultiGpuSync::<f32>::new(context, MultiGpuConfig::default(), 1024)
1194 .expect("construction");
1195 assert!(sync.synchronize_all().is_ok());
1196 }
1197
1198 #[test]
1199 fn multi_gpu_setup_opens_a_real_backend_not_the_removed_cuda_one() {
1200 match MultiGpuSetup::new(2, 1024) {
1201 Ok(setup) => {
1202 assert_eq!(setup.contexts.len(), 2);
1203 assert_eq!(setup.sync_managers.len(), 2);
1204 for context in &setup.contexts {
1205 assert_ne!(
1206 context.backend(),
1207 scirs2_core::gpu::GpuBackend::Cuda,
1208 "must never request the CUDA backend scirs2-core 0.6.x always errors on"
1209 );
1210 }
1211 }
1212 Err(e) => {
1213 eprintln!(
1215 "SKIP: multi_gpu_setup_opens_a_real_backend_not_the_removed_cuda_one — {e}"
1216 );
1217 }
1218 }
1219 }
1220}