Skip to main content

scirs2_integrate/
parallel_optimization.rs

1//! Advanced parallel processing optimization for numerical algorithms
2//!
3//! This module provides sophisticated parallel processing strategies including
4//! work-stealing task distribution, NUMA-aware memory allocation, vectorized
5//! operations, and dynamic load balancing for numerical integration algorithms.
6
7use 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
15/// Advanced parallel execution engine
16pub struct ParallelOptimizer {
17    /// Number of worker threads
18    pub num_threads: usize,
19    /// Thread pool for task execution
20    thread_pool: Option<ThreadPool>,
21    /// NUMA topology information
22    pub numa_info: NumaTopology,
23    /// Load balancing strategy
24    pub load_balancer: LoadBalancingStrategy,
25    /// Work-stealing configuration
26    pub work_stealing_config: WorkStealingConfig,
27}
28
29/// Thread pool with advanced features
30pub struct ThreadPool {
31    workers: Vec<Worker>,
32    task_queue: Arc<Mutex<TaskQueue>>,
33    shutdown: Arc<AtomicUsize>,
34}
35
36/// Individual worker thread
37struct Worker {
38    id: usize,
39    thread: Option<JoinHandle<()>>,
40    local_queue: Arc<Mutex<LocalTaskQueue>>,
41}
42
43/// Main task queue for work distribution
44struct TaskQueue {
45    global_tasks: Vec<Box<dyn ParallelTask + Send>>,
46    pending_tasks: usize,
47}
48
49/// Local task queue for each worker
50struct LocalTaskQueue {
51    tasks: Vec<Box<dyn ParallelTask + Send>>,
52    steals_attempted: usize,
53    steals_successful: usize,
54}
55
56/// NUMA (Non-Uniform Memory Access) topology information
57#[derive(Debug, Clone)]
58pub struct NumaTopology {
59    /// Number of NUMA nodes
60    pub num_nodes: usize,
61    /// CPU cores per NUMA node
62    pub cores_per_node: Vec<usize>,
63    /// Memory bandwidth per node
64    pub bandwidth_per_node: Vec<f64>,
65    /// Memory latency between nodes
66    pub inter_node_latency: Vec<Vec<f64>>,
67}
68
69/// Load balancing strategies
70#[derive(Debug, Clone, Copy)]
71pub enum LoadBalancingStrategy {
72    /// Static load balancing
73    Static,
74    /// Dynamic load balancing based on runtime metrics
75    Dynamic,
76    /// Work-stealing between threads
77    WorkStealing,
78    /// NUMA-aware load balancing
79    NumaAware,
80    /// Adaptive strategy that switches based on workload
81    Adaptive,
82}
83
84/// Work-stealing configuration
85#[derive(Debug, Clone)]
86pub struct WorkStealingConfig {
87    /// Maximum number of steal attempts before yielding
88    pub max_steal_attempts: usize,
89    /// Steal ratio (fraction of work to steal)
90    pub steal_ratio: f64,
91    /// Minimum task size to enable stealing
92    pub min_steal_size: usize,
93    /// Backoff strategy for failed steals
94    pub backoff_strategy: BackoffStrategy,
95}
96
97/// Backoff strategies for work stealing
98#[derive(Debug, Clone, Copy)]
99pub enum BackoffStrategy {
100    /// No backoff
101    None,
102    /// Linear backoff
103    Linear(Duration),
104    /// Exponential backoff
105    Exponential { initial: Duration, max: Duration },
106    /// Random jitter backoff
107    RandomJitter { min: Duration, max: Duration },
108}
109
110/// Trait for parallel tasks
111pub trait ParallelTask: Send {
112    /// Execute the task
113    fn execute(&self) -> ParallelResult;
114
115    /// Estimate computational cost
116    fn estimated_cost(&self) -> f64;
117
118    /// Check if task can be subdivided
119    fn can_subdivide(&self) -> bool;
120
121    /// Subdivide task into smaller tasks
122    fn subdivide(&self) -> Vec<Box<dyn ParallelTask + Send>>;
123
124    /// Get task priority
125    fn priority(&self) -> TaskPriority {
126        TaskPriority::Normal
127    }
128
129    /// Get preferred NUMA node
130    fn preferred_numa_node(&self) -> Option<usize> {
131        None
132    }
133}
134
135/// Task execution result
136pub type ParallelResult = IntegrateResult<Box<dyn std::any::Any + Send>>;
137
138/// Task priority levels
139#[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
147/// Vectorized computation task
148pub struct VectorizedComputeTask<F: IntegrateFloat> {
149    /// Input data
150    pub input: Array2<F>,
151    /// Operation to perform
152    pub operation: VectorOperation<F>,
153    /// Chunk size for processing
154    pub chunk_size: usize,
155    /// SIMD preference
156    pub prefer_simd: bool,
157}
158
159/// Types of vectorized operations
160#[derive(Clone)]
161pub enum VectorOperation<F: IntegrateFloat> {
162    /// Element-wise arithmetic
163    ElementWise(ArithmeticOp),
164    /// Matrix-vector operations
165    MatrixVector(Array1<F>),
166    /// Reduction operations
167    Reduction(ReductionOp),
168    /// Custom function
169    Custom(Arc<dyn Fn(&ArrayView2<F>) -> Array2<F> + Send + Sync>),
170}
171
172/// Arithmetic operations
173#[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/// Reduction operations
185#[derive(Debug, Clone, Copy)]
186pub enum ReductionOp {
187    Sum,
188    Product,
189    Max,
190    Min,
191    Mean,
192    Variance,
193}
194
195/// NUMA-aware memory allocator
196pub struct NumaAllocator {
197    /// Node affinities for threads
198    node_affinities: Vec<usize>,
199    /// Memory usage per node
200    memory_usage: Vec<AtomicUsize>,
201    /// Allocation strategy
202    strategy: NumaAllocationStrategy,
203}
204
205/// NUMA allocation strategies
206#[derive(Debug, Clone, Copy)]
207pub enum NumaAllocationStrategy {
208    /// First-touch allocation
209    FirstTouch,
210    /// Round-robin allocation
211    RoundRobin,
212    /// Local allocation (preferred node)
213    Local,
214    /// Interleaved allocation
215    Interleaved,
216}
217
218/// Parallel execution statistics
219#[derive(Debug, Clone)]
220pub struct ParallelExecutionStats {
221    /// Total execution time
222    pub total_time: Duration,
223    /// Time per thread
224    pub thread_times: Vec<Duration>,
225    /// Load balance efficiency
226    pub load_balance_efficiency: f64,
227    /// Work stealing statistics
228    pub work_stealing_stats: WorkStealingStats,
229    /// NUMA affinity hits
230    pub numa_affinity_hits: usize,
231    /// Cache performance metrics
232    pub cache_performance: CachePerformanceMetrics,
233    /// SIMD utilization
234    pub simd_utilization: f64,
235}
236
237/// Work stealing performance statistics
238#[derive(Debug, Clone)]
239pub struct WorkStealingStats {
240    /// Total steal attempts
241    pub steal_attempts: usize,
242    /// Successful steals
243    pub successful_steals: usize,
244    /// Average steal success rate
245    pub success_rate: f64,
246    /// Time spent on stealing vs working
247    pub steal_time_ratio: f64,
248}
249
250/// Cache performance metrics.
251///
252/// These fields require CPU performance counters to populate. The
253/// [`ParallelOptimizer`] execution path does not sample such counters, so it
254/// reports them as zero ("not measured") rather than fabricating values.
255#[derive(Debug, Clone)]
256pub struct CachePerformanceMetrics {
257    /// Estimated cache hit rate (0.0 when not measured)
258    pub hit_rate: f64,
259    /// Memory bandwidth utilization (0.0 when not measured)
260    pub bandwidth_utilization: f64,
261    /// Cache-friendly access patterns detected (0 when not measured)
262    pub cache_friendly_accesses: usize,
263}
264
265impl ParallelOptimizer {
266    /// Create new parallel optimizer
267    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    /// Initialize thread pool
278    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    /// Execute tasks in parallel with optimization
285    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        // Optimize task distribution based on strategy
296        let optimized_tasks = self.optimize_task_distribution(tasks)?;
297
298        // Execute tasks
299        let results = self
300            .thread_pool
301            .as_ref()
302            .expect("Failed to create parallel plan")
303            .execute_tasks(optimized_tasks)?;
304
305        // Collect statistics
306        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    /// Optimize task distribution based on load balancing strategy
315    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                // Simple round-robin distribution
322                Ok(tasks
323                    .into_iter()
324                    .map(|t| t as Box<dyn ParallelTask + Send>)
325                    .collect())
326            }
327            LoadBalancingStrategy::Dynamic => {
328                // Sort by estimated cost and distribute
329                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                // Enable subdivisions for large tasks
341                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                // Group tasks by preferred NUMA node
354                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                // Distribute no-preference tasks evenly
371                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                // Choose strategy based on task characteristics
380                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                    // Use work-stealing for expensive tasks
385                    self.load_balancer = LoadBalancingStrategy::WorkStealing;
386                } else if tasks.iter().any(|t| t.preferred_numa_node().is_some()) {
387                    // Use NUMA-aware for tasks with locality preferences
388                    self.load_balancer = LoadBalancingStrategy::NumaAware;
389                } else {
390                    // Use dynamic for other cases
391                    self.load_balancer = LoadBalancingStrategy::Dynamic;
392                }
393
394                self.optimize_task_distribution(tasks)
395            }
396        }
397    }
398
399    /// Collect execution statistics.
400    ///
401    /// Only genuinely observable quantities are reported here; nothing is
402    /// fabricated. Concretely:
403    ///
404    /// * `total_time` is the real wall-clock duration of the parallel section.
405    /// * `work_stealing_stats` are read from each worker's live counters. Note
406    ///   that the data-parallel execution path ([`ThreadPool::execute_tasks`])
407    ///   dispatches work through the core `parallel_ops` runtime rather than the
408    ///   persistent worker loop, so these counters report the actual steal
409    ///   activity of the explicit work-stealing path (which is zero when all
410    ///   work was handled by the data-parallel runtime).
411    /// * Per-thread wall-clock attribution (`thread_times`) is **not** available
412    ///   for the data-parallel execution path, so an empty vector is returned
413    ///   rather than a fabricated per-thread duration.
414    /// * Hardware-level quantities (NUMA affinity hits, cache hit/bandwidth,
415    ///   SIMD utilisation) require CPU performance counters that are not
416    ///   sampled here; they are reported as zero and documented as "not
417    ///   measured" rather than fabricated with plausible numbers.
418    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        // Aggregate the real work-stealing counters maintained by the worker
426        // local queues. These are honest measurements: they reflect exactly the
427        // number of steal attempts/successes recorded by the explicit
428        // work-stealing path.
429        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            // The fraction of time spent stealing versus working is not
448            // instrumented; reported as zero rather than fabricated.
449            steal_time_ratio: 0.0,
450        };
451
452        // Per-thread wall-clock times are not observable for the data-parallel
453        // execution path; return an empty set rather than fabricate durations.
454        // With no per-thread samples there is no measured imbalance, so report
455        // perfect (1.0) load-balance efficiency as the neutral value.
456        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 require hardware/OS counters that are not
465            // sampled here (not measured).
466            numa_affinity_hits: 0,
467            cache_performance: CachePerformanceMetrics {
468                // Cache hit-rate / bandwidth / friendly-access counts require
469                // CPU performance counters that are not sampled here
470                // (not measured).
471                hit_rate: 0.0,
472                bandwidth_utilization: 0.0,
473                cache_friendly_accesses: 0,
474            },
475            // SIMD utilisation requires instruction-level profiling that is not
476            // performed here (not measured).
477            simd_utilization: 0.0,
478        })
479    }
480
481    /// Execute vectorized computation with SIMD optimization
482    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        // Process in chunks for cache efficiency
491        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                    // Broadcast back to chunk shape
503                    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    /// Apply element-wise operation with SIMD optimization
517    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    /// Apply matrix-vector operation
540    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        // Parallel matrix-vector multiplication
554        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    /// Apply reduction operation
564    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    /// Detect NUMA topology
589    pub fn detect() -> Self {
590        // Simplified NUMA detection - in practice would use hwloc or similar
591        let num_cores = thread::available_parallelism()
592            .map(|n| n.get())
593            .unwrap_or(1);
594        let num_nodes = (num_cores / 4).max(1); // Assume 4 cores per node
595
596        Self {
597            num_nodes,
598            cores_per_node: vec![4; num_nodes],
599            bandwidth_per_node: vec![100.0; num_nodes], // GB/s
600            inter_node_latency: vec![vec![1.0; num_nodes]; num_nodes], // μs
601        }
602    }
603
604    /// Get preferred NUMA node for current thread
605    pub fn get_preferred_node(&self) -> usize {
606        // Simple round-robin assignment
607        // Simple thread-to-NUMA mapping
608        0 // Simplified - would use proper thread ID mapping
609    }
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    /// Create new thread pool
628    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    /// Execute tasks in parallel and collect their real results.
668    ///
669    /// Large tasks are subdivided (when `can_subdivide()` and the estimated
670    /// cost warrants it) and the resulting work items are sorted largest-first
671    /// for better load balancing before being dispatched across the available
672    /// CPU threads via the data-parallel runtime.  Each returned
673    /// [`ParallelResult`] is the genuine output of `ParallelTask::execute`;
674    /// there is one entry per executed (sub)task, in dispatch order.
675    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        // Subdivide large tasks first for better load distribution.
686        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        // Sort work items by estimated cost (largest first) so that the most
696        // expensive items are scheduled before the cheap ones.
697        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        // Execute every work item and collect the genuine result of each one.
704        // `execute()` cannot fail at the dispatch level, so the only errors are
705        // those reported by the tasks themselves (preserved inside each
706        // `ParallelResult`).
707        let results: Vec<ParallelResult> = all_tasks
708            .into_par_iter()
709            .map(|task| task.execute())
710            .collect();
711
712        Ok(results)
713    }
714
715    /// Shutdown the thread pool and wait for all threads to complete
716    pub fn shutdown(&mut self) -> IntegrateResult<()> {
717        // Signal all threads to shutdown
718        self.shutdown.store(1, Ordering::Relaxed);
719
720        // Wait for all threads to complete
721        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    /// Try to steal work from other workers (simplified implementation)
735    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        // In a full implementation, we'd need access to other workers' queues
741        // For now, increment steal attempts counter and try global _queue again
742        if let Ok(mut local_q) = local_queue.lock() {
743            local_q.steals_attempted += 1;
744        }
745
746        // Try global _queue one more time as fallback
747        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    /// Worker thread main loop
762    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            // Check for shutdown signal
770            if shutdown.load(Ordering::Relaxed) == 1 {
771                break;
772            }
773
774            // Try to get a task from local _queue first
775            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 no local task, try global _queue
781            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 still no task, try work stealing from other workers
791            if task_option.is_none() {
792                task_option = Self::try_work_stealing(_worker_id, &local_queue, &global_queue);
793            }
794
795            // Execute task if found
796            if let Some(task) = task_option {
797                let _result = task.execute();
798                // Task executed successfully
799            } else {
800                // No work available, sleep briefly
801                thread::sleep(Duration::from_millis(1));
802            }
803        }
804    }
805}
806
807impl Drop for ThreadPool {
808    fn drop(&mut self) {
809        // Signal shutdown
810        self.shutdown.store(1, Ordering::Relaxed);
811
812        // Wait for threads to complete
813        for worker in self.workers.drain(..) {
814            if let Some(thread) = worker.thread {
815                let _ = thread.join(); // Ignore errors during cleanup
816            }
817        }
818    }
819}
820
821impl<F: IntegrateFloat + Send + Sync> ParallelTask for VectorizedComputeTask<F> {
822    fn execute(&self) -> ParallelResult {
823        // Perform actual vectorized computation based on operation type
824        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        // Only subdivide if the task is large enough and can benefit from parallelization
889        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        // `execute_parallel` must return the genuine output of each task, not a
960        // placeholder `()`. We submit a small (non-subdivided) element-wise
961        // task and verify the real computed array comes back.
962        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, // larger than the input so the task is not subdivided
968            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}