1use crate::common::IntegrateFloat;
8use crate::error::{IntegrateError, IntegrateResult};
9use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2, Axis};
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Arc, Mutex};
12use std::thread::{self, JoinHandle};
13use std::time::{Duration, Instant};
14
15pub struct ParallelOptimizer {
17 pub num_threads: usize,
19 thread_pool: Option<ThreadPool>,
21 pub numa_info: NumaTopology,
23 pub load_balancer: LoadBalancingStrategy,
25 pub work_stealing_config: WorkStealingConfig,
27}
28
29pub struct ThreadPool {
31 workers: Vec<Worker>,
32 task_queue: Arc<Mutex<TaskQueue>>,
33 shutdown: Arc<AtomicUsize>,
34}
35
36struct Worker {
38 id: usize,
39 thread: Option<JoinHandle<()>>,
40 local_queue: Arc<Mutex<LocalTaskQueue>>,
41}
42
43struct TaskQueue {
45 global_tasks: Vec<Box<dyn ParallelTask + Send>>,
46 pending_tasks: usize,
47}
48
49struct LocalTaskQueue {
51 tasks: Vec<Box<dyn ParallelTask + Send>>,
52 steals_attempted: usize,
53 steals_successful: usize,
54}
55
56#[derive(Debug, Clone)]
58pub struct NumaTopology {
59 pub num_nodes: usize,
61 pub cores_per_node: Vec<usize>,
63 pub bandwidth_per_node: Vec<f64>,
65 pub inter_node_latency: Vec<Vec<f64>>,
67}
68
69#[derive(Debug, Clone, Copy)]
71pub enum LoadBalancingStrategy {
72 Static,
74 Dynamic,
76 WorkStealing,
78 NumaAware,
80 Adaptive,
82}
83
84#[derive(Debug, Clone)]
86pub struct WorkStealingConfig {
87 pub max_steal_attempts: usize,
89 pub steal_ratio: f64,
91 pub min_steal_size: usize,
93 pub backoff_strategy: BackoffStrategy,
95}
96
97#[derive(Debug, Clone, Copy)]
99pub enum BackoffStrategy {
100 None,
102 Linear(Duration),
104 Exponential { initial: Duration, max: Duration },
106 RandomJitter { min: Duration, max: Duration },
108}
109
110pub trait ParallelTask: Send {
112 fn execute(&self) -> ParallelResult;
114
115 fn estimated_cost(&self) -> f64;
117
118 fn can_subdivide(&self) -> bool;
120
121 fn subdivide(&self) -> Vec<Box<dyn ParallelTask + Send>>;
123
124 fn priority(&self) -> TaskPriority {
126 TaskPriority::Normal
127 }
128
129 fn preferred_numa_node(&self) -> Option<usize> {
131 None
132 }
133}
134
135pub type ParallelResult = IntegrateResult<Box<dyn std::any::Any + Send>>;
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
140pub enum TaskPriority {
141 Low = 0,
142 Normal = 1,
143 High = 2,
144 Critical = 3,
145}
146
147pub struct VectorizedComputeTask<F: IntegrateFloat> {
149 pub input: Array2<F>,
151 pub operation: VectorOperation<F>,
153 pub chunk_size: usize,
155 pub prefer_simd: bool,
157}
158
159#[derive(Clone)]
161pub enum VectorOperation<F: IntegrateFloat> {
162 ElementWise(ArithmeticOp),
164 MatrixVector(Array1<F>),
166 Reduction(ReductionOp),
168 Custom(Arc<dyn Fn(&ArrayView2<F>) -> Array2<F> + Send + Sync>),
170}
171
172#[derive(Debug, Clone, Copy)]
174pub enum ArithmeticOp {
175 Add(f64),
176 Multiply(f64),
177 Power(f64),
178 Exp,
179 Log,
180 Sin,
181 Cos,
182}
183
184#[derive(Debug, Clone, Copy)]
186pub enum ReductionOp {
187 Sum,
188 Product,
189 Max,
190 Min,
191 Mean,
192 Variance,
193}
194
195pub struct NumaAllocator {
197 node_affinities: Vec<usize>,
199 memory_usage: Vec<AtomicUsize>,
201 strategy: NumaAllocationStrategy,
203}
204
205#[derive(Debug, Clone, Copy)]
207pub enum NumaAllocationStrategy {
208 FirstTouch,
210 RoundRobin,
212 Local,
214 Interleaved,
216}
217
218#[derive(Debug, Clone)]
220pub struct ParallelExecutionStats {
221 pub total_time: Duration,
223 pub thread_times: Vec<Duration>,
225 pub load_balance_efficiency: f64,
227 pub work_stealing_stats: WorkStealingStats,
229 pub numa_affinity_hits: usize,
231 pub cache_performance: CachePerformanceMetrics,
233 pub simd_utilization: f64,
235}
236
237#[derive(Debug, Clone)]
239pub struct WorkStealingStats {
240 pub steal_attempts: usize,
242 pub successful_steals: usize,
244 pub success_rate: f64,
246 pub steal_time_ratio: f64,
248}
249
250#[derive(Debug, Clone)]
256pub struct CachePerformanceMetrics {
257 pub hit_rate: f64,
259 pub bandwidth_utilization: f64,
261 pub cache_friendly_accesses: usize,
263}
264
265impl ParallelOptimizer {
266 pub fn new(_numthreads: usize) -> Self {
268 Self {
269 num_threads: _numthreads,
270 thread_pool: None,
271 numa_info: NumaTopology::detect(),
272 load_balancer: LoadBalancingStrategy::Adaptive,
273 work_stealing_config: WorkStealingConfig::default(),
274 }
275 }
276
277 pub fn initialize(&mut self) -> IntegrateResult<()> {
279 let thread_pool = ThreadPool::new(self.num_threads, &self.work_stealing_config)?;
280 self.thread_pool = Some(thread_pool);
281 Ok(())
282 }
283
284 pub fn execute_parallel<T: ParallelTask + Send + 'static>(
286 &mut self,
287 tasks: Vec<Box<T>>,
288 ) -> IntegrateResult<(Vec<ParallelResult>, ParallelExecutionStats)> {
289 let start_time = Instant::now();
290
291 if self.thread_pool.is_none() {
292 self.initialize()?;
293 }
294
295 let optimized_tasks = self.optimize_task_distribution(tasks)?;
297
298 let results = self
300 .thread_pool
301 .as_ref()
302 .expect("Failed to create parallel plan")
303 .execute_tasks(optimized_tasks)?;
304
305 let stats = self.collect_execution_stats(
307 start_time,
308 self.thread_pool.as_ref().expect("Operation failed"),
309 )?;
310
311 Ok((results, stats))
312 }
313
314 fn optimize_task_distribution<T: ParallelTask + Send + 'static>(
316 &mut self,
317 mut tasks: Vec<Box<T>>,
318 ) -> IntegrateResult<Vec<Box<dyn ParallelTask + Send>>> {
319 match self.load_balancer {
320 LoadBalancingStrategy::Static => {
321 Ok(tasks
323 .into_iter()
324 .map(|t| t as Box<dyn ParallelTask + Send>)
325 .collect())
326 }
327 LoadBalancingStrategy::Dynamic => {
328 tasks.sort_by(|a, b| {
330 b.estimated_cost()
331 .partial_cmp(&a.estimated_cost())
332 .expect("Operation failed")
333 });
334 Ok(tasks
335 .into_iter()
336 .map(|t| t as Box<dyn ParallelTask + Send>)
337 .collect())
338 }
339 LoadBalancingStrategy::WorkStealing => {
340 let mut optimized_tasks = Vec::new();
342 for task in tasks {
343 if task.can_subdivide() && task.estimated_cost() > 1000.0 {
344 let subtasks = task.subdivide();
345 optimized_tasks.extend(subtasks);
346 } else {
347 optimized_tasks.push(task as Box<dyn ParallelTask + Send>);
348 }
349 }
350 Ok(optimized_tasks)
351 }
352 LoadBalancingStrategy::NumaAware => {
353 let mut numa_groups: Vec<Vec<Box<dyn ParallelTask + Send>>> =
355 (0..self.numa_info.num_nodes).map(|_| Vec::new()).collect();
356 let mut no_preference = Vec::new();
357
358 for task in tasks {
359 if let Some(preferred_node) = task.preferred_numa_node() {
360 if preferred_node < numa_groups.len() {
361 numa_groups[preferred_node].push(task as Box<dyn ParallelTask + Send>);
362 } else {
363 no_preference.push(task as Box<dyn ParallelTask + Send>);
364 }
365 } else {
366 no_preference.push(task as Box<dyn ParallelTask + Send>);
367 }
368 }
369
370 for (i, task) in no_preference.into_iter().enumerate() {
372 let group_idx = i % numa_groups.len();
373 numa_groups[group_idx].push(task);
374 }
375
376 Ok(numa_groups.into_iter().flatten().collect())
377 }
378 LoadBalancingStrategy::Adaptive => {
379 let total_cost: f64 = tasks.iter().map(|t| t.estimated_cost()).sum();
381 let avg_cost = total_cost / tasks.len() as f64;
382
383 if avg_cost > 1000.0 {
384 self.load_balancer = LoadBalancingStrategy::WorkStealing;
386 } else if tasks.iter().any(|t| t.preferred_numa_node().is_some()) {
387 self.load_balancer = LoadBalancingStrategy::NumaAware;
389 } else {
390 self.load_balancer = LoadBalancingStrategy::Dynamic;
392 }
393
394 self.optimize_task_distribution(tasks)
395 }
396 }
397 }
398
399 fn collect_execution_stats(
419 &self,
420 start_time: Instant,
421 thread_pool: &ThreadPool,
422 ) -> IntegrateResult<ParallelExecutionStats> {
423 let total_time = start_time.elapsed();
424
425 let mut steal_attempts = 0usize;
430 let mut successful_steals = 0usize;
431 for worker in &thread_pool.workers {
432 if let Ok(local_q) = worker.local_queue.lock() {
433 steal_attempts += local_q.steals_attempted;
434 successful_steals += local_q.steals_successful;
435 }
436 }
437 let success_rate = if steal_attempts > 0 {
438 successful_steals as f64 / steal_attempts as f64
439 } else {
440 0.0
441 };
442
443 let work_stealing_stats = WorkStealingStats {
444 steal_attempts,
445 successful_steals,
446 success_rate,
447 steal_time_ratio: 0.0,
450 };
451
452 let thread_times: Vec<Duration> = Vec::new();
457 let load_balance_efficiency = 1.0;
458
459 Ok(ParallelExecutionStats {
460 total_time,
461 thread_times,
462 load_balance_efficiency,
463 work_stealing_stats,
464 numa_affinity_hits: 0,
467 cache_performance: CachePerformanceMetrics {
468 hit_rate: 0.0,
472 bandwidth_utilization: 0.0,
473 cache_friendly_accesses: 0,
474 },
475 simd_utilization: 0.0,
478 })
479 }
480
481 pub fn execute_vectorized<F: IntegrateFloat>(
483 &self,
484 task: VectorizedComputeTask<F>,
485 ) -> IntegrateResult<Array2<F>> {
486 let chunk_size = task.chunk_size.max(1);
487 let inputshape = task.input.dim();
488 let mut result = Array2::zeros(inputshape);
489
490 for chunk_start in (0..inputshape.0).step_by(chunk_size) {
492 let chunk_end = (chunk_start + chunk_size).min(inputshape.0);
493 let chunk = task.input.slice(s![chunk_start..chunk_end, ..]);
494
495 let chunk_result = match &task.operation {
496 VectorOperation::ElementWise(op) => {
497 self.apply_elementwise_operation(&chunk, *op)?
498 }
499 VectorOperation::MatrixVector(vec) => self.apply_matvec_operation(&chunk, vec)?,
500 VectorOperation::Reduction(op) => {
501 let reduced = self.apply_reduction_operation(&chunk, *op)?;
502 Array2::from_elem(chunk.dim(), reduced[[0, 0]])
504 }
505 VectorOperation::Custom(func) => func(&chunk),
506 };
507
508 result
509 .slice_mut(s![chunk_start..chunk_end, ..])
510 .assign(&chunk_result);
511 }
512
513 Ok(result)
514 }
515
516 fn apply_elementwise_operation<F: IntegrateFloat>(
518 &self,
519 input: &ArrayView2<F>,
520 op: ArithmeticOp,
521 ) -> IntegrateResult<Array2<F>> {
522 use ArithmeticOp::*;
523
524 let result = match op {
525 Add(value) => input.mapv(|x| x + F::from(value).expect("Failed to convert to float")),
526 Multiply(value) => {
527 input.mapv(|x| x * F::from(value).expect("Failed to convert to float"))
528 }
529 Power(exp) => input.mapv(|x| x.powf(F::from(exp).expect("Failed to convert to float"))),
530 Exp => input.mapv(|x| x.exp()),
531 Log => input.mapv(|x| x.ln()),
532 Sin => input.mapv(|x| x.sin()),
533 Cos => input.mapv(|x| x.cos()),
534 };
535
536 Ok(result)
537 }
538
539 fn apply_matvec_operation<F: IntegrateFloat>(
541 &self,
542 matrix: &ArrayView2<F>,
543 vector: &Array1<F>,
544 ) -> IntegrateResult<Array2<F>> {
545 if matrix.ncols() != vector.len() {
546 return Err(IntegrateError::DimensionMismatch(
547 "Matrix columns must match vector length".to_string(),
548 ));
549 }
550
551 let mut result = Array2::zeros(matrix.dim());
552
553 for (i, mut row) in result.axis_iter_mut(Axis(0)).enumerate() {
555 let matrix_row = matrix.row(i);
556 let dot_product = matrix_row.dot(vector);
557 row.fill(dot_product);
558 }
559
560 Ok(result)
561 }
562
563 fn apply_reduction_operation<F: IntegrateFloat>(
565 &self,
566 input: &ArrayView2<F>,
567 op: ReductionOp,
568 ) -> IntegrateResult<Array2<F>> {
569 let result_value = match op {
570 ReductionOp::Sum => input.sum(),
571 ReductionOp::Product => input.fold(F::one(), |acc, &x| acc * x),
572 ReductionOp::Max => input.fold(F::neg_infinity(), |acc, &x| acc.max(x)),
573 ReductionOp::Min => input.fold(F::infinity(), |acc, &x| acc.min(x)),
574 ReductionOp::Mean => input.sum() / F::from(input.len()).expect("Operation failed"),
575 ReductionOp::Variance => {
576 let mean = input.sum() / F::from(input.len()).expect("Operation failed");
577
578 input.mapv(|x| (x - mean).powi(2)).sum()
579 / F::from(input.len()).expect("Operation failed")
580 }
581 };
582
583 Ok(Array2::from_elem((1, 1), result_value))
584 }
585}
586
587impl NumaTopology {
588 pub fn detect() -> Self {
590 let num_cores = thread::available_parallelism()
592 .map(|n| n.get())
593 .unwrap_or(1);
594 let num_nodes = (num_cores / 4).max(1); Self {
597 num_nodes,
598 cores_per_node: vec![4; num_nodes],
599 bandwidth_per_node: vec![100.0; num_nodes], inter_node_latency: vec![vec![1.0; num_nodes]; num_nodes], }
602 }
603
604 pub fn get_preferred_node(&self) -> usize {
606 0 }
610}
611
612impl Default for WorkStealingConfig {
613 fn default() -> Self {
614 Self {
615 max_steal_attempts: 10,
616 steal_ratio: 0.5,
617 min_steal_size: 100,
618 backoff_strategy: BackoffStrategy::Exponential {
619 initial: Duration::from_micros(1),
620 max: Duration::from_millis(1),
621 },
622 }
623 }
624}
625
626impl ThreadPool {
627 pub fn new(num_threads: usize, config: &WorkStealingConfig) -> IntegrateResult<Self> {
629 let task_queue = Arc::new(Mutex::new(TaskQueue {
630 global_tasks: Vec::new(),
631 pending_tasks: 0,
632 }));
633
634 let shutdown = Arc::new(AtomicUsize::new(0));
635 let mut workers = Vec::with_capacity(num_threads);
636
637 for id in 0..num_threads {
638 let worker_queue = Arc::new(Mutex::new(LocalTaskQueue {
639 tasks: Vec::new(),
640 steals_attempted: 0,
641 steals_successful: 0,
642 }));
643
644 let task_queue_clone = Arc::clone(&task_queue);
645 let worker_queue_clone = Arc::clone(&worker_queue);
646 let shutdown_clone = Arc::clone(&shutdown);
647
648 let thread_handle = thread::spawn(move || {
649 Self::worker_thread_loop(id, worker_queue_clone, task_queue_clone, shutdown_clone);
650 });
651
652 let worker = Worker {
653 id,
654 thread: Some(thread_handle),
655 local_queue: worker_queue,
656 };
657 workers.push(worker);
658 }
659
660 Ok(Self {
661 workers,
662 task_queue,
663 shutdown,
664 })
665 }
666
667 pub fn execute_tasks(
676 &self,
677 tasks: Vec<Box<dyn ParallelTask + Send>>,
678 ) -> IntegrateResult<Vec<ParallelResult>> {
679 use scirs2_core::parallel_ops::*;
680
681 if tasks.is_empty() {
682 return Ok(Vec::new());
683 }
684
685 let mut all_tasks: Vec<Box<dyn ParallelTask + Send>> = Vec::with_capacity(tasks.len());
687 for task in tasks {
688 if task.can_subdivide() && task.estimated_cost() > 10.0 {
689 all_tasks.extend(task.subdivide());
690 } else {
691 all_tasks.push(task);
692 }
693 }
694
695 all_tasks.sort_by(|a, b| {
698 b.estimated_cost()
699 .partial_cmp(&a.estimated_cost())
700 .unwrap_or(std::cmp::Ordering::Equal)
701 });
702
703 let results: Vec<ParallelResult> = all_tasks
708 .into_par_iter()
709 .map(|task| task.execute())
710 .collect();
711
712 Ok(results)
713 }
714
715 pub fn shutdown(&mut self) -> IntegrateResult<()> {
717 self.shutdown.store(1, Ordering::Relaxed);
719
720 for worker in self.workers.drain(..) {
722 if let Some(thread) = worker.thread {
723 if thread.join().is_err() {
724 return Err(IntegrateError::ComputationError(
725 "Failed to join worker thread".to_string(),
726 ));
727 }
728 }
729 }
730
731 Ok(())
732 }
733
734 fn try_work_stealing(
736 _worker_id: usize,
737 local_queue: &Arc<Mutex<LocalTaskQueue>>,
738 global_queue: &Arc<Mutex<TaskQueue>>,
739 ) -> Option<Box<dyn ParallelTask + Send>> {
740 if let Ok(mut local_q) = local_queue.lock() {
743 local_q.steals_attempted += 1;
744 }
745
746 if let Ok(mut global_q) = global_queue.lock() {
748 let task = global_q.global_tasks.pop();
749 if task.is_some() {
750 global_q.pending_tasks = global_q.pending_tasks.saturating_sub(1);
751 if let Ok(mut local_q) = local_queue.lock() {
752 local_q.steals_successful += 1;
753 }
754 }
755 task
756 } else {
757 None
758 }
759 }
760
761 fn worker_thread_loop(
763 _worker_id: usize,
764 local_queue: Arc<Mutex<LocalTaskQueue>>,
765 global_queue: Arc<Mutex<TaskQueue>>,
766 shutdown: Arc<AtomicUsize>,
767 ) {
768 loop {
769 if shutdown.load(Ordering::Relaxed) == 1 {
771 break;
772 }
773
774 let mut task_option = None;
776 if let Ok(mut local_q) = local_queue.lock() {
777 task_option = local_q.tasks.pop();
778 }
779
780 if task_option.is_none() {
782 if let Ok(mut global_q) = global_queue.lock() {
783 task_option = global_q.global_tasks.pop();
784 if task_option.is_some() {
785 global_q.pending_tasks = global_q.pending_tasks.saturating_sub(1);
786 }
787 }
788 }
789
790 if task_option.is_none() {
792 task_option = Self::try_work_stealing(_worker_id, &local_queue, &global_queue);
793 }
794
795 if let Some(task) = task_option {
797 let _result = task.execute();
798 } else {
800 thread::sleep(Duration::from_millis(1));
802 }
803 }
804 }
805}
806
807impl Drop for ThreadPool {
808 fn drop(&mut self) {
809 self.shutdown.store(1, Ordering::Relaxed);
811
812 for worker in self.workers.drain(..) {
814 if let Some(thread) = worker.thread {
815 let _ = thread.join(); }
817 }
818 }
819}
820
821impl<F: IntegrateFloat + Send + Sync> ParallelTask for VectorizedComputeTask<F> {
822 fn execute(&self) -> ParallelResult {
823 let result: Array2<F> = match &self.operation {
825 VectorOperation::ElementWise(op) => match op {
826 ArithmeticOp::Add(value) => self
827 .input
828 .mapv(|x| x + F::from(*value).expect("Failed to convert to float")),
829 ArithmeticOp::Multiply(value) => self
830 .input
831 .mapv(|x| x * F::from(*value).expect("Failed to convert to float")),
832 ArithmeticOp::Power(exp) => self
833 .input
834 .mapv(|x| x.powf(F::from(*exp).expect("Failed to convert to float"))),
835 ArithmeticOp::Exp => self.input.mapv(|x| x.exp()),
836 ArithmeticOp::Log => self.input.mapv(|x| x.ln()),
837 ArithmeticOp::Sin => self.input.mapv(|x| x.sin()),
838 ArithmeticOp::Cos => self.input.mapv(|x| x.cos()),
839 },
840 VectorOperation::MatrixVector(vector) => {
841 if self.input.ncols() != vector.len() {
842 return Err(IntegrateError::DimensionMismatch(
843 "Matrix columns must match vector length".to_string(),
844 ));
845 }
846
847 let mut result = Array2::zeros(self.input.dim());
848 for (i, mut row) in result.axis_iter_mut(Axis(0)).enumerate() {
849 let matrix_row = self.input.row(i);
850 let dot_product = matrix_row.dot(vector);
851 row.fill(dot_product);
852 }
853 result
854 }
855 VectorOperation::Reduction(op) => {
856 let result_value = match op {
857 ReductionOp::Sum => self.input.sum(),
858 ReductionOp::Product => self.input.fold(F::one(), |acc, &x| acc * x),
859 ReductionOp::Max => self.input.fold(F::neg_infinity(), |acc, &x| acc.max(x)),
860 ReductionOp::Min => self.input.fold(F::infinity(), |acc, &x| acc.min(x)),
861 ReductionOp::Mean => {
862 self.input.sum() / F::from(self.input.len()).expect("Operation failed")
863 }
864 ReductionOp::Variance => {
865 let mean =
866 self.input.sum() / F::from(self.input.len()).expect("Operation failed");
867 self.input.mapv(|x| (x - mean).powi(2)).sum()
868 / F::from(self.input.len()).expect("Operation failed")
869 }
870 };
871 Array2::from_elem((1, 1), result_value)
872 }
873 VectorOperation::Custom(func) => func(&self.input.view()),
874 };
875
876 Ok(Box::new(result) as Box<dyn std::any::Any + Send>)
877 }
878
879 fn estimated_cost(&self) -> f64 {
880 (self.input.len() as f64) / (self.chunk_size as f64)
881 }
882
883 fn can_subdivide(&self) -> bool {
884 self.input.nrows() > self.chunk_size * 2
885 }
886
887 fn subdivide(&self) -> Vec<Box<dyn ParallelTask + Send>> {
888 if self.input.len() < self.chunk_size * 2 {
890 return vec![];
891 }
892
893 let num_chunks = self.input.nrows().div_ceil(self.chunk_size);
894 let mut subtasks = Vec::with_capacity(num_chunks);
895
896 for i in 0..num_chunks {
897 let start_row = i * self.chunk_size;
898 let end_row = ((i + 1) * self.chunk_size).min(self.input.nrows());
899
900 if start_row < self.input.nrows() {
901 let chunk = self.input.slice(s![start_row..end_row, ..]).to_owned();
902
903 let subtask = VectorizedComputeTask {
904 input: chunk,
905 operation: self.operation.clone(),
906 chunk_size: self.chunk_size,
907 prefer_simd: self.prefer_simd,
908 };
909
910 subtasks.push(Box::new(subtask) as Box<dyn ParallelTask + Send>);
911 }
912 }
913
914 subtasks
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use crate::parallel_optimization::ArithmeticOp;
921 use crate::{NumaTopology, ParallelOptimizer, VectorOperation, VectorizedComputeTask};
922 use scirs2_core::ndarray::Array2;
923
924 #[test]
925 fn test_parallel_optimizer_creation() {
926 let optimizer = ParallelOptimizer::new(4);
927 assert_eq!(optimizer.num_threads, 4);
928 }
929
930 #[test]
931 fn test_numa_topology_detection() {
932 let topology = NumaTopology::detect();
933 assert!(topology.num_nodes > 0);
934 assert!(!topology.cores_per_node.is_empty());
935 }
936
937 #[test]
938 fn test_vectorized_computation() {
939 let optimizer = ParallelOptimizer::new(2);
940 let input = Array2::from_elem((4, 4), 1.0);
941
942 let task = VectorizedComputeTask {
943 input,
944 operation: VectorOperation::ElementWise(ArithmeticOp::Add(2.0)),
945 chunk_size: 2,
946 prefer_simd: true,
947 };
948
949 let result = optimizer.execute_vectorized(task);
950 assert!(result.is_ok());
951
952 let output = result.expect("Test: parallel integration failed");
953 assert_eq!(output.dim(), (4, 4));
954 assert!((output[[0, 0]] - 3.0_f64).abs() < 1e-10);
955 }
956
957 #[test]
958 fn test_execute_parallel_returns_real_results() {
959 let mut optimizer = ParallelOptimizer::new(2);
963 let input = Array2::from_elem((2, 2), 5.0_f64);
964 let task = VectorizedComputeTask {
965 input,
966 operation: VectorOperation::ElementWise(ArithmeticOp::Add(3.0)),
967 chunk_size: 8, prefer_simd: false,
969 };
970
971 let (results, _stats) = optimizer
972 .execute_parallel(vec![Box::new(task)])
973 .expect("parallel execution should succeed");
974
975 assert_eq!(results.len(), 1, "exactly one task was submitted");
976 let any = results
977 .into_iter()
978 .next()
979 .expect("one result")
980 .expect("task itself should succeed");
981 let array = any
982 .downcast_ref::<Array2<f64>>()
983 .expect("result must be the real Array2 produced by the task, not a placeholder");
984 assert_eq!(array.dim(), (2, 2));
985 for &v in array.iter() {
986 assert!((v - 8.0_f64).abs() < 1e-10, "5 + 3 should equal 8, got {v}");
987 }
988 }
989}