1use crate::error::ScirsResult;
45use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
46use std::sync::Arc;
47
48use crate::distributed::{
49 DistributedConfig, DistributedOptimizationContext, DistributedStats, MPIInterface,
50};
51use crate::gpu::{
52 acceleration::{AccelerationConfig, AccelerationManager},
53 cuda_kernels::DifferentialEvolutionKernel,
54 tensor_core_optimization::{AMPManager, TensorCoreOptimizationConfig, TensorCoreOptimizer},
55 GpuOptimizationConfig, GpuOptimizationContext,
56};
57use crate::result::OptimizeResults;
58use statrs::statistics::Statistics;
59
60#[derive(Clone)]
62pub struct DistributedGpuConfig {
63 pub distributed_config: DistributedConfig,
65 pub gpu_config: GpuOptimizationConfig,
67 pub acceleration_config: AccelerationConfig,
69 pub use_tensor_cores: bool,
71 pub tensor_config: Option<TensorCoreOptimizationConfig>,
73 pub gpu_communication_strategy: GpuCommunicationStrategy,
75 pub gpu_cpu_load_balance: f64, }
78
79impl Default for DistributedGpuConfig {
80 fn default() -> Self {
81 Self {
82 distributed_config: DistributedConfig::default(),
83 gpu_config: GpuOptimizationConfig::default(),
84 acceleration_config: AccelerationConfig::default(),
85 use_tensor_cores: true,
86 tensor_config: Some(TensorCoreOptimizationConfig::default()),
87 gpu_communication_strategy: GpuCommunicationStrategy::Direct,
88 gpu_cpu_load_balance: 0.8, }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum GpuCommunicationStrategy {
96 Direct,
98 Staged,
100 AsyncOverlapped,
102 Hierarchical,
104}
105
106pub struct DistributedGpuOptimizer<M: MPIInterface> {
108 distributed_context: DistributedOptimizationContext<M>,
109 gpu_context: GpuOptimizationContext,
110 acceleration_manager: AccelerationManager,
111 tensor_optimizer: Option<TensorCoreOptimizer>,
112 amp_manager: Option<AMPManager>,
113 config: DistributedGpuConfig,
114 performance_stats: DistributedGpuStats,
115}
116
117impl<M: MPIInterface> DistributedGpuOptimizer<M> {
118 pub fn new(mpi: M, config: DistributedGpuConfig) -> ScirsResult<Self> {
120 let distributed_context =
121 DistributedOptimizationContext::new(mpi, config.distributed_config.clone());
122 let gpu_context = GpuOptimizationContext::new(config.gpu_config.clone())?;
123 let acceleration_manager = AccelerationManager::new(config.acceleration_config.clone());
124
125 let tensor_optimizer = if config.use_tensor_cores {
126 match config.tensor_config.as_ref() {
127 Some(tensor_config) => {
128 match TensorCoreOptimizer::new(
129 gpu_context.context().clone(),
130 tensor_config.clone(),
131 ) {
132 Ok(optimizer) => Some(optimizer),
133 Err(_) => {
134 None
136 }
137 }
138 }
139 None => None,
140 }
141 } else {
142 None
143 };
144
145 let amp_manager = if config
146 .tensor_config
147 .as_ref()
148 .map(|tc| tc.use_amp)
149 .unwrap_or(false)
150 {
151 Some(AMPManager::new())
152 } else {
153 None
154 };
155
156 Ok(Self {
157 distributed_context,
158 gpu_context,
159 acceleration_manager,
160 tensor_optimizer,
161 amp_manager,
162 config,
163 performance_stats: DistributedGpuStats::new(),
164 })
165 }
166
167 pub fn differential_evolution<F>(
169 &mut self,
170 function: F,
171 bounds: &[(f64, f64)],
172 population_size: usize,
173 max_nit: usize,
174 ) -> ScirsResult<DistributedGpuResults>
175 where
176 F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
177 {
178 let start_time = std::time::Instant::now();
179
180 let work_assignment = self.distributed_context.distribute_work(population_size);
182 let local_pop_size = work_assignment.count;
183
184 if local_pop_size == 0 {
185 return Ok(DistributedGpuResults::empty()); }
187
188 let dims = bounds.len();
190 let mut local_population = self.initialize_gpu_population(local_pop_size, bounds)?;
191 let mut local_fitness = self.evaluate_population_gpu(&function, &local_population)?;
192
193 let evolution_kernel =
203 DifferentialEvolutionKernel::new(Arc::clone(self.gpu_context.context()))?;
204
205 let mut best_individual = Array1::zeros(dims);
206 let mut best_fitness = f64::INFINITY;
207 let mut total_evaluations = local_pop_size;
208
209 for iteration in 0..max_nit {
211 let trial_population = self.gpu_mutation_crossover(
213 &evolution_kernel,
214 &local_population,
215 0.8, 0.7, )?;
218
219 let trial_fitness = self.evaluate_population_gpu(&function, &trial_population)?;
221 total_evaluations += local_pop_size;
222
223 self.gpu_selection(
225 &evolution_kernel,
226 &mut local_population,
227 &trial_population,
228 &mut local_fitness,
229 &trial_fitness,
230 )?;
231
232 let (local_best_idx, local_best_fitness) = self.find_local_best(&local_fitness)?;
234
235 if local_best_fitness < best_fitness {
236 best_fitness = local_best_fitness;
237 best_individual = local_population.row(local_best_idx).to_owned();
238 }
239
240 if iteration % 10 == 0 {
242 let global_best =
243 self.communicate_best_individuals(&best_individual, best_fitness)?;
244
245 if let Some((global_best_individual, global_best_fitness)) = global_best {
246 if global_best_fitness < best_fitness {
247 best_individual = global_best_individual;
248 best_fitness = global_best_fitness;
249 }
250 }
251
252 self.gpu_migration(&mut local_population, &mut local_fitness)?;
254 }
255
256 self.performance_stats.record_iteration(
258 iteration,
259 local_pop_size,
260 best_fitness,
261 start_time.elapsed().as_secs_f64(),
262 );
263
264 if self.check_convergence(&local_fitness, iteration)? {
266 break;
267 }
268 }
269
270 let final_global_best =
272 self.communicate_best_individuals(&best_individual, best_fitness)?;
273
274 if let Some((final_best_individual, final_best_fitness)) = final_global_best {
275 best_individual = final_best_individual;
276 best_fitness = final_best_fitness;
277 }
278
279 let total_time = start_time.elapsed().as_secs_f64();
280
281 Ok(DistributedGpuResults {
282 base_result: OptimizeResults::<f64> {
283 x: best_individual,
284 fun: best_fitness,
285 success: true,
286 message: "Distributed GPU differential evolution completed".to_string(),
287 nit: max_nit,
288 nfev: total_evaluations,
289 ..OptimizeResults::default()
290 },
291 gpu_stats: crate::gpu::acceleration::PerformanceStats::default(),
292 distributed_stats: self.distributed_context.stats().clone(),
293 performance_stats: self.performance_stats.clone(),
294 total_time,
295 })
296 }
297
298 fn initialize_gpu_population(
300 &self,
301 pop_size: usize,
302 bounds: &[(f64, f64)],
303 ) -> ScirsResult<Array2<f64>> {
304 use scirs2_core::random::{Rng, RngExt};
305 let mut rng = scirs2_core::random::rng();
306
307 let dims = bounds.len();
308 let mut population = Array2::zeros((pop_size, dims));
309
310 for i in 0..pop_size {
311 for j in 0..dims {
312 let (low, high) = bounds[j];
313 population[[i, j]] = rng.random_range(low..=high);
314 }
315 }
316
317 Ok(population)
318 }
319
320 fn evaluate_population_gpu<F>(
322 &mut self,
323 function: &F,
324 population: &Array2<f64>,
325 ) -> ScirsResult<Array1<f64>>
326 where
327 F: Fn(&ArrayView1<f64>) -> f64,
328 {
329 let pop_size = population.nrows();
330 let mut fitness = Array1::zeros(pop_size);
331
332 for i in 0..pop_size {
335 fitness[i] = function(&population.row(i));
336 }
337
338 let use_gpu = pop_size >= gpu_kernels::GPU_DISTRIBUTED_THRESHOLD
343 && self.config.gpu_cpu_load_balance > 0.5;
344
345 if use_gpu && gpu_kernels::gpu_batch_reduction_ok(fitness.as_slice()) {
346 self.performance_stats.gpu_evaluations += pop_size;
349 } else {
350 self.performance_stats.cpu_evaluations += pop_size;
352 }
353
354 Ok(fitness)
355 }
356
357 fn gpu_mutation_crossover(
369 &self,
370 _kernel: &DifferentialEvolutionKernel,
371 population: &Array2<f64>,
372 f_scale: f64,
373 crossover_rate: f64,
374 ) -> ScirsResult<Array2<f64>> {
375 let (pop_size, dims) = population.dim();
376 let mut trial_population = Array2::zeros((pop_size, dims));
377
378 use scirs2_core::random::{Rng, RngExt};
379 let mut rng = scirs2_core::random::rng();
380
381 let mut donor_indices: Vec<[usize; 3]> = Vec::with_capacity(pop_size);
384 let mut j_rand_values: Vec<usize> = Vec::with_capacity(pop_size);
385 for i in 0..pop_size {
386 let mut indices = Vec::new();
387 while indices.len() < 3 {
388 let idx = rng.random_range(0..pop_size);
389 if idx != i && !indices.contains(&idx) {
390 indices.push(idx);
391 }
392 }
393 donor_indices.push([indices[0], indices[1], indices[2]]);
394 j_rand_values.push(rng.random_range(0..dims));
395 }
396
397 let donor = gpu_kernels::donor_matrix(population, &donor_indices, f_scale)?;
400
401 for i in 0..pop_size {
403 let j_rand = j_rand_values[i];
404 for j in 0..dims {
405 if rng.random_range(0.0..1.0) < crossover_rate || j == j_rand {
406 trial_population[[i, j]] = donor[[i, j]];
407 } else {
408 trial_population[[i, j]] = population[[i, j]];
409 }
410 }
411 }
412
413 Ok(trial_population)
414 }
415
416 fn gpu_selection(
428 &self,
429 _kernel: &DifferentialEvolutionKernel,
430 population: &mut Array2<f64>,
431 trial_population: &Array2<f64>,
432 fitness: &mut Array1<f64>,
433 trial_fitness: &Array1<f64>,
434 ) -> ScirsResult<()> {
435 let deltas = gpu_kernels::fitness_delta(
437 trial_fitness.as_slice(),
438 fitness.as_slice(),
439 trial_fitness.len(),
440 )?;
441
442 for i in 0..population.nrows() {
443 if deltas[i] <= 0.0 {
445 for j in 0..population.ncols() {
446 population[[i, j]] = trial_population[[i, j]];
447 }
448 fitness[i] = trial_fitness[i];
449 }
450 }
451
452 Ok(())
453 }
454
455 fn find_local_best(&self, fitness: &Array1<f64>) -> ScirsResult<(usize, f64)> {
457 let mut best_idx = 0;
458 let mut best_fitness = fitness[0];
459
460 for (i, &f) in fitness.iter().enumerate() {
461 if f < best_fitness {
462 best_fitness = f;
463 best_idx = i;
464 }
465 }
466
467 Ok((best_idx, best_fitness))
468 }
469
470 fn communicate_best_individuals(
472 &mut self,
473 local_best: &Array1<f64>,
474 local_best_fitness: f64,
475 ) -> ScirsResult<Option<(Array1<f64>, f64)>> {
476 if self.distributed_context.size() <= 1 {
477 return Ok(None);
478 }
479
480 Ok(Some((local_best.clone(), local_best_fitness)))
486 }
487
488 fn gpu_migration(
490 &mut self,
491 population: &mut Array2<f64>,
492 fitness: &mut Array1<f64>,
493 ) -> ScirsResult<()> {
494 match self.config.gpu_communication_strategy {
495 GpuCommunicationStrategy::Direct => {
496 self.gpu_direct_migration(population, fitness)
498 }
499 GpuCommunicationStrategy::Staged => {
500 self.staged_migration(population, fitness)
502 }
503 GpuCommunicationStrategy::AsyncOverlapped => {
504 self.async_migration(population, fitness)
506 }
507 GpuCommunicationStrategy::Hierarchical => {
508 self.hierarchical_migration(population, fitness)
510 }
511 }
512 }
513
514 fn gpu_direct_migration(
516 &mut self,
517 population: &mut Array2<f64>,
518 _fitness: &mut Array1<f64>,
519 ) -> ScirsResult<()> {
520 Ok(())
523 }
524
525 fn staged_migration(
527 &mut self,
528 population: &mut Array2<f64>,
529 _fitness: &mut Array1<f64>,
530 ) -> ScirsResult<()> {
531 Ok(())
534 }
535
536 fn async_migration(
538 &mut self,
539 population: &mut Array2<f64>,
540 _fitness: &mut Array1<f64>,
541 ) -> ScirsResult<()> {
542 Ok(())
544 }
545
546 fn hierarchical_migration(
548 &mut self,
549 population: &mut Array2<f64>,
550 _fitness: &mut Array1<f64>,
551 ) -> ScirsResult<()> {
552 Ok(())
554 }
555
556 fn check_convergence(&self, fitness: &Array1<f64>, iteration: usize) -> ScirsResult<bool> {
558 if fitness.len() < 2 {
559 return Ok(false);
560 }
561
562 let mean = fitness.view().mean();
563 let variance =
564 fitness.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / fitness.len() as f64;
565
566 let std_dev = variance.sqrt();
567
568 Ok(std_dev < 1e-12 || iteration >= 1000)
570 }
571
572 fn generate_random_indices(&self, pop_size: usize) -> ScirsResult<Array2<i32>> {
574 use scirs2_core::random::{Rng, RngExt};
575 let mut rng = scirs2_core::random::rng();
576 let mut indices = Array2::zeros((pop_size, 3));
577
578 for i in 0..pop_size {
579 let mut selected = std::collections::HashSet::new();
580 selected.insert(i);
581
582 for j in 0..3 {
583 loop {
584 let idx = rng.random_range(0..pop_size);
585 if !selected.contains(&idx) {
586 indices[[i, j]] = idx as i32;
587 selected.insert(idx);
588 break;
589 }
590 }
591 }
592 }
593
594 Ok(indices)
595 }
596
597 fn generate_random_values(&self, count: usize) -> ScirsResult<Array1<f64>> {
599 use scirs2_core::random::{Rng, RngExt};
600 let mut rng = scirs2_core::random::rng();
601 let mut values = Array1::zeros(count);
602
603 for i in 0..count {
604 values[i] = rng.random_range(0.0..1.0);
605 }
606
607 Ok(values)
608 }
609
610 fn generate_j_rand(&self, pop_size: usize, dims: usize) -> ScirsResult<Array1<i32>> {
612 use scirs2_core::random::{Rng, RngExt};
613 let mut rng = scirs2_core::random::rng();
614 let mut j_rand = Array1::zeros(pop_size);
615
616 for i in 0..pop_size {
617 j_rand[i] = rng.random_range(0..dims) as i32;
618 }
619
620 Ok(j_rand)
621 }
622
623 pub fn stats(&self) -> &DistributedGpuStats {
625 &self.performance_stats
626 }
627}
628
629#[derive(Debug, Clone)]
631pub struct DistributedGpuStats {
632 pub gpu_evaluations: usize,
634 pub cpu_evaluations: usize,
636 pub gpu_utilization: f64,
638 pub communication_time: f64,
640 pub gpu_memory_usage: f64,
642 pub nit: Vec<IterationStats>,
644}
645
646impl DistributedGpuStats {
647 fn new() -> Self {
648 Self {
649 gpu_evaluations: 0,
650 cpu_evaluations: 0,
651 gpu_utilization: 0.0,
652 communication_time: 0.0,
653 gpu_memory_usage: 0.0,
654 nit: Vec::new(),
655 }
656 }
657
658 fn record_iteration(
659 &mut self,
660 iteration: usize,
661 pop_size: usize,
662 best_fitness: f64,
663 elapsed_time: f64,
664 ) {
665 self.nit.push(IterationStats {
666 iteration,
667 population_size: pop_size,
668 best_fitness,
669 elapsed_time,
670 });
671 }
672
673 pub fn generate_report(&self) -> String {
675 let mut report = String::from("Distributed GPU Optimization Performance Report\n");
676 report.push_str("==============================================\n\n");
677
678 report.push_str(&format!(
679 "GPU Function Evaluations: {}\n",
680 self.gpu_evaluations
681 ));
682 report.push_str(&format!(
683 "CPU Function Evaluations: {}\n",
684 self.cpu_evaluations
685 ));
686
687 let total_evaluations = self.gpu_evaluations + self.cpu_evaluations;
688 if total_evaluations > 0 {
689 let gpu_percentage = (self.gpu_evaluations as f64 / total_evaluations as f64) * 100.0;
690 report.push_str(&format!("GPU Usage: {:.1}%\n", gpu_percentage));
691 }
692
693 report.push_str(&format!(
694 "GPU Utilization: {:.1}%\n",
695 self.gpu_utilization * 100.0
696 ));
697 report.push_str(&format!(
698 "Communication Overhead: {:.3}s\n",
699 self.communication_time
700 ));
701 report.push_str(&format!(
702 "GPU Memory Usage: {:.1}%\n",
703 self.gpu_memory_usage * 100.0
704 ));
705
706 if let Some(last_iteration) = self.nit.last() {
707 report.push_str(&format!(
708 "Final Best Fitness: {:.6e}\n",
709 last_iteration.best_fitness
710 ));
711 report.push_str(&format!(
712 "Total Time: {:.3}s\n",
713 last_iteration.elapsed_time
714 ));
715 }
716
717 report
718 }
719}
720
721#[derive(Debug, Clone)]
723pub struct IterationStats {
724 pub iteration: usize,
725 pub population_size: usize,
726 pub best_fitness: f64,
727 pub elapsed_time: f64,
728}
729
730#[derive(Debug, Clone)]
732pub struct DistributedGpuResults {
733 pub base_result: OptimizeResults<f64>,
735 pub gpu_stats: crate::gpu::acceleration::PerformanceStats,
737 pub distributed_stats: DistributedStats,
739 pub performance_stats: DistributedGpuStats,
741 pub total_time: f64,
743}
744
745impl DistributedGpuResults {
746 fn empty() -> Self {
747 Self {
748 base_result: OptimizeResults::<f64> {
749 x: Array1::zeros(0),
750 fun: 0.0,
751 success: false,
752 message: "No work assigned to this process".to_string(),
753 nit: 0,
754 nfev: 0,
755 ..OptimizeResults::default()
756 },
757 gpu_stats: crate::gpu::acceleration::PerformanceStats::default(),
758 distributed_stats: DistributedStats {
759 communication_time: 0.0,
760 computation_time: 0.0,
761 load_balance_ratio: 1.0,
762 synchronizations: 0,
763 bytes_transferred: 0,
764 },
765 performance_stats: DistributedGpuStats::new(),
766 total_time: 0.0,
767 }
768 }
769
770 pub fn print_summary(&self) {
772 println!("Distributed GPU Optimization Results");
773 println!("===================================");
774 println!("Success: {}", self.base_result.success);
775 println!("Final function value: {:.6e}", self.base_result.fun);
776 println!("Iterations: {}", self.base_result.nit);
777 println!("Function evaluations: {}", self.base_result.nfev);
778 println!("Total time: {:.3}s", self.total_time);
779 println!();
780
781 println!("GPU Performance:");
782 println!("{}", self.gpu_stats.generate_report());
783 println!();
784
785 println!("Distributed Performance:");
786 println!("{}", self.distributed_stats.generate_report());
787 println!();
788
789 println!("Combined Performance:");
790 println!("{}", self.performance_stats.generate_report());
791 }
792}
793
794mod gpu_kernels {
807 use scirs2_core::ndarray::Array2;
808
809 pub(super) const GPU_DISTRIBUTED_THRESHOLD: usize = 4096;
815
816 #[cfg(feature = "gpu")]
821 const GPU_REDUCTION_REL_TOL: f64 = 1e-3;
822
823 #[cfg(feature = "gpu")]
828 fn gpu_context() -> Option<std::sync::Arc<scirs2_core::gpu::backends::WebGPUContext>> {
829 use scirs2_core::array_protocol::gpu_ndarray::{global_context, is_gpu_available};
830 use std::sync::OnceLock;
831
832 static PROBE: OnceLock<bool> = OnceLock::new();
833 let available = *PROBE.get_or_init(is_gpu_available);
834 if !available {
835 return None;
836 }
837 global_context()
838 }
839
840 pub(super) fn gpu_batch_reduction_ok(fitness: Option<&[f64]>) -> bool {
850 let fitness = match fitness {
851 Some(f) if f.len() >= GPU_DISTRIBUTED_THRESHOLD => f,
852 _ => return false,
853 };
854 gpu_batch_reduction_inner(fitness)
855 }
856
857 #[cfg(feature = "gpu")]
858 fn gpu_batch_reduction_inner(fitness: &[f64]) -> bool {
859 use scirs2_core::array_protocol::gpu_ndarray::GpuNdarray;
860
861 let ctx = match gpu_context() {
862 Some(c) => c,
863 None => return false,
864 };
865
866 let data_f32: Vec<f32> = fitness.iter().map(|&v| v as f32).collect();
867 let len = data_f32.len();
868 let gpu = match GpuNdarray::from_ndarray_data(&data_f32, vec![len], ctx) {
869 Ok(g) => g,
870 Err(_) => return false,
871 };
872 let gpu_sum = match gpu.sum_all() {
873 Ok(s) => f64::from(s),
874 Err(_) => return false,
875 };
876
877 let cpu_sum: f64 = data_f32.iter().map(|&v| f64::from(v)).sum();
881 let scale = cpu_sum.abs().max(1.0);
882 (gpu_sum - cpu_sum).abs() <= GPU_REDUCTION_REL_TOL * scale
883 }
884
885 #[cfg(not(feature = "gpu"))]
886 fn gpu_batch_reduction_inner(_fitness: &[f64]) -> bool {
887 false
888 }
889
890 pub(super) fn donor_matrix(
898 population: &Array2<f64>,
899 donor_indices: &[[usize; 3]],
900 f_scale: f64,
901 ) -> crate::error::ScirsResult<Array2<f64>> {
902 let (pop_size, dims) = population.dim();
903 let total = pop_size.saturating_mul(dims);
904
905 #[cfg(feature = "gpu")]
906 {
907 if total >= GPU_DISTRIBUTED_THRESHOLD {
908 if let Some(donor) = donor_matrix_gpu(population, donor_indices, f_scale) {
909 return Ok(donor);
910 }
911 }
912 }
913 #[cfg(not(feature = "gpu"))]
914 {
915 let _ = total;
916 }
917 Ok(donor_matrix_cpu(population, donor_indices, f_scale))
918 }
919
920 fn donor_matrix_cpu(
922 population: &Array2<f64>,
923 donor_indices: &[[usize; 3]],
924 f_scale: f64,
925 ) -> Array2<f64> {
926 let (pop_size, dims) = population.dim();
927 let mut donor = Array2::zeros((pop_size, dims));
928 for (i, &[a, b, c]) in donor_indices.iter().enumerate().take(pop_size) {
929 for j in 0..dims {
930 donor[[i, j]] =
931 population[[a, j]] + f_scale * (population[[b, j]] - population[[c, j]]);
932 }
933 }
934 donor
935 }
936
937 #[cfg(feature = "gpu")]
940 fn donor_matrix_gpu(
941 population: &Array2<f64>,
942 donor_indices: &[[usize; 3]],
943 f_scale: f64,
944 ) -> Option<Array2<f64>> {
945 use scirs2_core::array_protocol::gpu_ndarray::GpuNdarray;
946
947 let (pop_size, dims) = population.dim();
948 let total = pop_size.checked_mul(dims)?;
949 let ctx = gpu_context()?;
950
951 let mut a_flat: Vec<f32> = Vec::with_capacity(total);
953 let mut b_flat: Vec<f32> = Vec::with_capacity(total);
954 let mut c_flat: Vec<f32> = Vec::with_capacity(total);
955 for &[a, b, c] in donor_indices.iter().take(pop_size) {
956 for j in 0..dims {
957 a_flat.push(population[[a, j]] as f32);
958 b_flat.push(population[[b, j]] as f32);
959 c_flat.push(population[[c, j]] as f32);
960 }
961 }
962
963 let shape = vec![pop_size, dims];
964 let a_gpu =
965 GpuNdarray::from_ndarray_data(&a_flat, shape.clone(), std::sync::Arc::clone(&ctx))
966 .ok()?;
967 let b_gpu =
968 GpuNdarray::from_ndarray_data(&b_flat, shape.clone(), std::sync::Arc::clone(&ctx))
969 .ok()?;
970 let c_gpu =
971 GpuNdarray::from_ndarray_data(&c_flat, shape, std::sync::Arc::clone(&ctx)).ok()?;
972
973 let diff = b_gpu.subtract(&c_gpu).ok()?;
975 let scaled = diff.multiply_by_scalar_f32(f_scale as f32).ok()?;
976 let donor_gpu = a_gpu.add(&scaled).ok()?;
977
978 let flat = donor_gpu.to_vec().ok()?;
979 if flat.len() != total {
980 return None;
981 }
982 let donor_f64: Vec<f64> = flat.into_iter().map(f64::from).collect();
983 Array2::from_shape_vec((pop_size, dims), donor_f64).ok()
984 }
985
986 pub(super) fn fitness_delta(
992 trial: Option<&[f64]>,
993 current: Option<&[f64]>,
994 len: usize,
995 ) -> crate::error::ScirsResult<Vec<f64>> {
996 let (trial, current) = match (trial, current) {
1000 (Some(t), Some(c)) if t.len() == len && c.len() == len => (t, c),
1001 _ => return Ok(vec![0.0; len]),
1002 };
1003
1004 #[cfg(feature = "gpu")]
1005 {
1006 if len >= GPU_DISTRIBUTED_THRESHOLD {
1007 if let Some(delta) = fitness_delta_gpu(trial, current) {
1008 return Ok(delta);
1009 }
1010 }
1011 }
1012 #[cfg(not(feature = "gpu"))]
1013 {
1014 let _ = len;
1015 }
1016 Ok(fitness_delta_cpu(trial, current))
1017 }
1018
1019 fn fitness_delta_cpu(trial: &[f64], current: &[f64]) -> Vec<f64> {
1021 trial
1022 .iter()
1023 .zip(current.iter())
1024 .map(|(&t, &c)| t - c)
1025 .collect()
1026 }
1027
1028 #[cfg(feature = "gpu")]
1030 fn fitness_delta_gpu(trial: &[f64], current: &[f64]) -> Option<Vec<f64>> {
1031 use scirs2_core::array_protocol::gpu_ndarray::GpuNdarray;
1032
1033 let len = trial.len();
1034 let ctx = gpu_context()?;
1035
1036 let trial_f32: Vec<f32> = trial.iter().map(|&v| v as f32).collect();
1037 let current_f32: Vec<f32> = current.iter().map(|&v| v as f32).collect();
1038
1039 let trial_gpu =
1040 GpuNdarray::from_ndarray_data(&trial_f32, vec![len], std::sync::Arc::clone(&ctx))
1041 .ok()?;
1042 let current_gpu =
1043 GpuNdarray::from_ndarray_data(¤t_f32, vec![len], std::sync::Arc::clone(&ctx))
1044 .ok()?;
1045
1046 let delta_gpu = trial_gpu.subtract(¤t_gpu).ok()?;
1047 let flat = delta_gpu.to_vec().ok()?;
1048 if flat.len() != len {
1049 return None;
1050 }
1051 Some(flat.into_iter().map(f64::from).collect())
1052 }
1053
1054 }
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062
1063 #[test]
1064 fn test_distributed_gpu_config() {
1065 let config = DistributedGpuConfig::default();
1066 assert!(config.use_tensor_cores);
1067 assert_eq!(config.gpu_cpu_load_balance, 0.8);
1068 assert_eq!(
1069 config.gpu_communication_strategy,
1070 GpuCommunicationStrategy::Direct
1071 );
1072 }
1073
1074 #[test]
1075 fn test_gpu_communication_strategies() {
1076 let strategies = [
1077 GpuCommunicationStrategy::Direct,
1078 GpuCommunicationStrategy::Staged,
1079 GpuCommunicationStrategy::AsyncOverlapped,
1080 GpuCommunicationStrategy::Hierarchical,
1081 ];
1082
1083 for strategy in &strategies {
1084 let mut config = DistributedGpuConfig::default();
1085 config.gpu_communication_strategy = *strategy;
1086 assert_eq!(config.gpu_communication_strategy, *strategy);
1088 }
1089 }
1090
1091 #[test]
1092 fn test_performance_stats() {
1093 let mut stats = DistributedGpuStats::new();
1094 stats.gpu_evaluations = 1000;
1095 stats.cpu_evaluations = 200;
1096 stats.gpu_utilization = 0.85;
1097
1098 let report = stats.generate_report();
1099 assert!(report.contains("GPU Function Evaluations: 1000"));
1100 assert!(report.contains("CPU Function Evaluations: 200"));
1101 assert!(report.contains("GPU Usage: 83.3%")); }
1103
1104 #[test]
1105 #[ignore = "Requires MPI and GPU"]
1106 fn test_distributed_gpu_optimization() {
1107 }
1110
1111 fn donor_matrix_reference(
1120 population: &Array2<f64>,
1121 donor_indices: &[[usize; 3]],
1122 f_scale: f64,
1123 ) -> Array2<f64> {
1124 let (pop_size, dims) = population.dim();
1125 let mut donor = Array2::zeros((pop_size, dims));
1126 for (i, &[a, b, c]) in donor_indices.iter().enumerate().take(pop_size) {
1127 for j in 0..dims {
1128 donor[[i, j]] =
1129 population[[a, j]] + f_scale * (population[[b, j]] - population[[c, j]]);
1130 }
1131 }
1132 donor
1133 }
1134
1135 fn sample_population(pop_size: usize, dims: usize) -> (Array2<f64>, Vec<[usize; 3]>) {
1137 let mut population = Array2::zeros((pop_size, dims));
1138 for i in 0..pop_size {
1139 for j in 0..dims {
1140 population[[i, j]] = ((i * dims + j) as f64).sin() * 3.0 + (i as f64) * 0.01;
1141 }
1142 }
1143 let donor_indices: Vec<[usize; 3]> = (0..pop_size)
1145 .map(|i| [(i + 1) % pop_size, (i + 2) % pop_size, (i + 3) % pop_size])
1146 .collect();
1147 (population, donor_indices)
1148 }
1149
1150 #[test]
1151 fn donor_matrix_cpu_fallback_matches_reference() {
1152 let (population, donor_indices) = sample_population(128, 64); let f_scale = 0.8;
1155
1156 let got = gpu_kernels::donor_matrix(&population, &donor_indices, f_scale)
1157 .expect("donor_matrix should never error");
1158 let expected = donor_matrix_reference(&population, &donor_indices, f_scale);
1159
1160 let max_diff = (&got - &expected)
1161 .mapv(f64::abs)
1162 .iter()
1163 .cloned()
1164 .fold(0.0f64, f64::max);
1165 assert!(
1167 max_diff < 1e-3,
1168 "donor matrix differs from reference by {max_diff:.2e}"
1169 );
1170 println!("donor_matrix_cpu_fallback_matches_reference: max diff = {max_diff:.2e}");
1171 }
1172
1173 #[test]
1174 fn fitness_delta_cpu_fallback_matches_reference() {
1175 let len = 5000usize; let trial: Vec<f64> = (0..len).map(|i| (i as f64).cos() * 2.0).collect();
1177 let current: Vec<f64> = (0..len).map(|i| (i as f64).sin()).collect();
1178
1179 let got = gpu_kernels::fitness_delta(Some(&trial), Some(¤t), len)
1180 .expect("fitness_delta should never error");
1181 assert_eq!(got.len(), len);
1182
1183 let max_diff = trial
1184 .iter()
1185 .zip(current.iter())
1186 .zip(got.iter())
1187 .map(|((&t, &c), &d)| (d - (t - c)).abs())
1188 .fold(0.0f64, f64::max);
1189 assert!(
1190 max_diff < 1e-3,
1191 "fitness delta differs from reference by {max_diff:.2e}"
1192 );
1193 println!("fitness_delta_cpu_fallback_matches_reference: max diff = {max_diff:.2e}");
1194 }
1195
1196 #[test]
1197 fn fitness_delta_below_threshold_is_exact() {
1198 let trial = [3.0, 1.0, 4.0, 1.0];
1200 let current = [1.0, 2.0, 3.0, 5.0];
1201 let got = gpu_kernels::fitness_delta(Some(&trial), Some(¤t), 4)
1202 .expect("fitness_delta should never error");
1203 assert_eq!(got, vec![2.0, -1.0, 1.0, -4.0]);
1204 }
1205
1206 #[test]
1207 fn fitness_delta_degenerate_inputs_are_safe() {
1208 let got =
1210 gpu_kernels::fitness_delta(None, None, 3).expect("fitness_delta should never error");
1211 assert_eq!(got, vec![0.0, 0.0, 0.0]);
1212 }
1213
1214 #[test]
1215 fn batch_reduction_below_threshold_is_cpu() {
1216 let fitness = [1.0, 2.0, 3.0];
1218 assert!(!gpu_kernels::gpu_batch_reduction_ok(Some(&fitness)));
1219 assert!(!gpu_kernels::gpu_batch_reduction_ok(None));
1220 }
1221
1222 #[cfg(feature = "gpu")]
1225 #[test]
1226 fn donor_matrix_gpu_matches_cpu_or_skips() {
1227 use scirs2_core::array_protocol::gpu_ndarray::is_gpu_available;
1228 if !is_gpu_available() {
1229 println!("No wgpu adapter available — skipping donor_matrix GPU test");
1230 return;
1231 }
1232 let (population, donor_indices) = sample_population(256, 64); let f_scale = 0.7;
1234
1235 let gpu = gpu_kernels::donor_matrix(&population, &donor_indices, f_scale)
1236 .expect("donor_matrix should never error");
1237 let cpu = donor_matrix_reference(&population, &donor_indices, f_scale);
1238
1239 let max_diff = (&gpu - &cpu)
1240 .mapv(f64::abs)
1241 .iter()
1242 .cloned()
1243 .fold(0.0f64, f64::max);
1244 assert!(
1245 max_diff < 1e-3,
1246 "GPU donor matrix differs from CPU by {max_diff:.2e} (exceeds f32 tol 1e-3)"
1247 );
1248 println!("donor_matrix_gpu_matches_cpu_or_skips passed: max diff = {max_diff:.2e}");
1249 }
1250
1251 #[cfg(feature = "gpu")]
1252 #[test]
1253 fn fitness_delta_gpu_matches_cpu_or_skips() {
1254 use scirs2_core::array_protocol::gpu_ndarray::is_gpu_available;
1255 if !is_gpu_available() {
1256 println!("No wgpu adapter available — skipping fitness_delta GPU test");
1257 return;
1258 }
1259 let len = 8192usize;
1260 let trial: Vec<f64> = (0..len).map(|i| (i as f64).cos() * 5.0 - 1.0).collect();
1261 let current: Vec<f64> = (0..len).map(|i| (i as f64).sin() * 2.0 + 0.5).collect();
1262
1263 let gpu = gpu_kernels::fitness_delta(Some(&trial), Some(¤t), len)
1264 .expect("fitness_delta should never error");
1265 let cpu: Vec<f64> = trial
1266 .iter()
1267 .zip(current.iter())
1268 .map(|(&t, &c)| t - c)
1269 .collect();
1270
1271 let max_diff = gpu
1272 .iter()
1273 .zip(cpu.iter())
1274 .map(|(&g, &c)| (g - c).abs())
1275 .fold(0.0f64, f64::max);
1276 assert!(
1277 max_diff < 1e-3,
1278 "GPU fitness delta differs from CPU by {max_diff:.2e} (exceeds f32 tol 1e-3)"
1279 );
1280 println!("fitness_delta_gpu_matches_cpu_or_skips passed: max diff = {max_diff:.2e}");
1281 }
1282
1283 #[cfg(feature = "gpu")]
1284 #[test]
1285 fn batch_reduction_gpu_matches_cpu_or_skips() {
1286 use scirs2_core::array_protocol::gpu_ndarray::is_gpu_available;
1287 if !is_gpu_available() {
1288 println!("No wgpu adapter available — skipping batch_reduction GPU test");
1289 return;
1290 }
1291 let fitness: Vec<f64> = (0..8192).map(|i| 1.0 + (i as f64).sin().abs()).collect();
1293 assert!(
1295 gpu_kernels::gpu_batch_reduction_ok(Some(&fitness)),
1296 "GPU batch reduction failed to validate against the CPU aggregate"
1297 );
1298 println!("batch_reduction_gpu_matches_cpu_or_skips passed");
1299 }
1300
1301 #[cfg(feature = "gpu")]
1305 #[test]
1306 fn distributed_de_gpu_path_runs_or_skips() {
1307 use crate::distributed::MockMPI;
1308
1309 let config = DistributedGpuConfig::default();
1310 let mut optimizer = match DistributedGpuOptimizer::new(MockMPI::new(0, 1), config) {
1311 Ok(o) => o,
1312 Err(e) => {
1313 println!("Could not create distributed GPU optimizer — skipping ({e})");
1314 return;
1315 }
1316 };
1317
1318 let sphere = |x: &ArrayView1<f64>| -> f64 { x.iter().map(|&xi| xi * xi).sum() };
1322 let bounds = vec![(-5.0, 5.0); 5];
1323
1324 let result = match optimizer.differential_evolution(sphere, &bounds, 200, 20) {
1325 Ok(r) => r,
1326 Err(e) => {
1327 println!("Distributed GPU DE returned an error — skipping ({e})");
1328 return;
1329 }
1330 };
1331
1332 let fun = result.base_result.fun;
1335 assert!(
1336 fun.is_finite() && fun < 50.0,
1337 "expected DE to make progress on the 5-D sphere, got f={fun:.4e}"
1338 );
1339 println!("distributed_de_gpu_path_runs_or_skips passed: f={fun:.4e}");
1340 }
1341}