Skip to main content

scirs2_linalg/parallel/thread_pool/
mod.rs

1//! Thread pool configurations for linear algebra operations
2//!
3//! This module provides flexible thread pool management with support for
4//! different configurations optimized for various linear algebra workloads.
5
6use super::configure_workers;
7use scirs2_core::parallel_ops::*;
8use std::sync::{Arc, Mutex, Once};
9
10/// Global thread pool manager
11static INIT: Once = Once::new();
12static mut GLOBAL_POOL: Option<Arc<Mutex<ThreadPoolManager>>> = None;
13
14/// Thread pool configuration profiles
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ThreadPoolProfile {
17    /// Default profile - uses system defaults
18    Default,
19    /// CPU-bound profile - one thread per CPU core
20    CpuBound,
21    /// Memory-bound profile - fewer threads to reduce memory contention
22    MemoryBound,
23    /// Latency-sensitive profile - more threads for better responsiveness
24    LatencySensitive,
25    /// Custom profile with specific thread count
26    Custom(usize),
27}
28
29impl ThreadPoolProfile {
30    /// Get the number of threads for this profile
31    pub fn num_threads(&self) -> usize {
32        match self {
33            ThreadPoolProfile::Default => std::thread::available_parallelism()
34                .map(|n| n.get())
35                .unwrap_or(4),
36            ThreadPoolProfile::CpuBound => std::thread::available_parallelism()
37                .map(|n| n.get())
38                .unwrap_or(4),
39            ThreadPoolProfile::MemoryBound => {
40                // Use half the available cores to reduce memory contention
41                std::thread::available_parallelism()
42                    .map(|n| std::cmp::max(1, n.get() / 2))
43                    .unwrap_or(2)
44            }
45            ThreadPoolProfile::LatencySensitive => {
46                // Use 1.5x the available cores for better responsiveness
47                std::thread::available_parallelism()
48                    .map(|n| n.get() + n.get() / 2)
49                    .unwrap_or(6)
50            }
51            ThreadPoolProfile::Custom(n) => *n,
52        }
53    }
54}
55
56/// Thread pool manager for linear algebra operations
57pub struct ThreadPoolManager {
58    profile: ThreadPoolProfile,
59    /// Stack size for worker threads (in bytes)
60    stacksize: Option<usize>,
61    /// Thread name prefix
62    thread_name_prefix: String,
63    /// Whether to pin threads to CPU cores
64    cpu_affinity: bool,
65}
66
67impl ThreadPoolManager {
68    /// Create a new thread pool manager with default settings
69    pub fn new() -> Self {
70        Self {
71            profile: ThreadPoolProfile::Default,
72            stacksize: None,
73            thread_name_prefix: "linalg-worker".to_string(),
74            cpu_affinity: false,
75        }
76    }
77
78    /// Set the thread pool profile
79    pub fn with_profile(mut self, profile: ThreadPoolProfile) -> Self {
80        self.profile = profile;
81        self
82    }
83
84    /// Set the stack size for worker threads
85    pub fn with_stacksize(mut self, size: usize) -> Self {
86        self.stacksize = Some(size);
87        self
88    }
89
90    /// Set the thread name prefix
91    pub fn with_thread_name_prefix(mut self, prefix: String) -> Self {
92        self.thread_name_prefix = prefix;
93        self
94    }
95
96    /// Enable CPU affinity for worker threads
97    pub fn with_cpu_affinity(mut self, enabled: bool) -> Self {
98        self.cpu_affinity = enabled;
99        self
100    }
101
102    /// Initialize the thread pool with current settings
103    pub fn initialize(&self) -> Result<(), String> {
104        let num_threads = self.profile.num_threads();
105
106        // Configure rayon thread pool
107        let thread_prefix = self.thread_name_prefix.clone();
108        let mut pool_builder = ThreadPoolBuilder::new()
109            .num_threads(num_threads)
110            .thread_name(move |idx| format!("{thread_prefix}-{idx}"));
111
112        if let Some(stacksize) = self.stacksize {
113            pool_builder = pool_builder.stack_size(stacksize);
114        }
115
116        pool_builder
117            .build_global()
118            .map_err(|e| format!("Failed to initialize thread pool: {e}"))?;
119
120        // Set OpenMP threads for BLAS/LAPACK operations
121        std::env::set_var("OMP_NUM_THREADS", num_threads.to_string());
122
123        // Set MKL threads if using Intel MKL
124        std::env::set_var("MKL_NUM_THREADS", num_threads.to_string());
125
126        Ok(())
127    }
128
129    /// Get current thread pool statistics
130    pub fn statistics(&self) -> ThreadPoolStats {
131        ThreadPoolStats {
132            num_threads: self.profile.num_threads(),
133            current_parallelism: num_threads(),
134            profile: self.profile,
135            stacksize: self.stacksize,
136        }
137    }
138}
139
140impl Default for ThreadPoolManager {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146/// Thread pool statistics
147#[derive(Debug, Clone)]
148pub struct ThreadPoolStats {
149    pub num_threads: usize,
150    pub current_parallelism: usize,
151    pub profile: ThreadPoolProfile,
152    pub stacksize: Option<usize>,
153}
154
155/// Get the global thread pool manager
156pub fn global_pool() -> Arc<Mutex<ThreadPoolManager>> {
157    unsafe {
158        INIT.call_once(|| {
159            GLOBAL_POOL = Some(Arc::new(Mutex::new(ThreadPoolManager::new())));
160        });
161        #[allow(static_mut_refs)]
162        GLOBAL_POOL.as_ref().expect("Operation failed").clone()
163    }
164}
165
166/// Initialize global thread pool with a specific profile
167pub fn initialize_global_pool(profile: ThreadPoolProfile) -> Result<(), String> {
168    let pool = global_pool();
169    let mut manager = pool.lock().expect("Operation failed");
170    manager.profile = profile;
171    manager.initialize()
172}
173
174/// Adaptive thread pool that adjusts based on workload
175pub struct AdaptiveThreadPool {
176    min_threads: usize,
177    max_threads: usize,
178    current_threads: Arc<Mutex<usize>>,
179    /// Tracks CPU utilization for adaptive scaling
180    cpu_utilization: Arc<Mutex<f64>>,
181}
182
183impl AdaptiveThreadPool {
184    /// Create a new adaptive thread pool
185    pub fn new(_min_threads: usize, maxthreads: usize) -> Self {
186        let current = std::thread::available_parallelism()
187            .map(|n| n.get())
188            .unwrap_or(4);
189
190        Self {
191            min_threads: _min_threads,
192            max_threads: maxthreads,
193            current_threads: Arc::new(Mutex::new(current)),
194            cpu_utilization: Arc::new(Mutex::new(0.0)),
195        }
196    }
197
198    /// Update thread count based on current utilization
199    pub fn adapt(&self, utilization: f64) {
200        let mut current = self.current_threads.lock().expect("Operation failed");
201        let mut cpu_util = self.cpu_utilization.lock().expect("Operation failed");
202        *cpu_util = utilization;
203
204        if utilization > 0.9 && *current < self.max_threads {
205            // High utilization - increase threads
206            *current = std::cmp::min(*current + 1, self.max_threads);
207            self.apply_thread_count(*current);
208        } else if utilization < 0.5 && *current > self.min_threads {
209            // Low utilization - decrease threads
210            *current = std::cmp::max(*current - 1, self.min_threads);
211            self.apply_thread_count(*current);
212        }
213    }
214
215    /// Apply the new thread count
216    fn apply_thread_count(&self, count: usize) {
217        configure_workers(Some(count));
218    }
219
220    /// Get current thread count
221    pub fn current_thread_count(&self) -> usize {
222        *self.current_threads.lock().expect("Operation failed")
223    }
224}
225
226/// Thread pool benchmarking utilities
227pub mod benchmark {
228    use super::*;
229    use std::time::{Duration, Instant};
230
231    /// Benchmark result for a thread pool configuration
232    #[derive(Debug, Clone)]
233    pub struct BenchmarkResult {
234        pub profile: ThreadPoolProfile,
235        pub num_threads: usize,
236        pub execution_time: Duration,
237        pub throughput: f64,
238    }
239
240    /// Benchmark different thread pool configurations
241    pub fn benchmark_configurations<F>(
242        profiles: &[ThreadPoolProfile],
243        workload: F,
244    ) -> Vec<BenchmarkResult>
245    where
246        F: Fn() -> f64 + Clone,
247    {
248        let mut results = Vec::new();
249
250        for &profile in profiles {
251            // Initialize thread pool with profile
252            if let Err(e) = initialize_global_pool(profile) {
253                eprintln!("Failed to initialize pool for {profile:?}: {e}");
254                continue;
255            }
256
257            // Warm up
258            for _ in 0..3 {
259                workload();
260            }
261
262            // Benchmark
263            let start = Instant::now();
264            let operations = 10;
265            let mut total_work = 0.0;
266
267            for _ in 0..operations {
268                total_work += workload();
269            }
270
271            let elapsed = start.elapsed();
272            let throughput = total_work / elapsed.as_secs_f64();
273
274            results.push(BenchmarkResult {
275                profile,
276                num_threads: profile.num_threads(),
277                execution_time: elapsed,
278                throughput,
279            });
280        }
281
282        results
283    }
284
285    /// Find optimal thread pool configuration for a workload
286    pub fn find_optimal_configuration<F>(workload: F) -> ThreadPoolProfile
287    where
288        F: Fn() -> f64 + Clone,
289    {
290        let profiles = vec![
291            ThreadPoolProfile::CpuBound,
292            ThreadPoolProfile::MemoryBound,
293            ThreadPoolProfile::LatencySensitive,
294        ];
295
296        let results = benchmark_configurations(&profiles, workload);
297
298        results
299            .into_iter()
300            .max_by(|a, b| {
301                a.throughput
302                    .partial_cmp(&b.throughput)
303                    .expect("Operation failed")
304            })
305            .map(|r| r.profile)
306            .unwrap_or(ThreadPoolProfile::Default)
307    }
308}
309
310/// Enhanced thread pool with advanced monitoring and scaling
311///
312/// This provides sophisticated thread pool management with real-time monitoring,
313/// dynamic scaling, and intelligent load balancing for optimal performance.
314pub struct EnhancedThreadPool {
315    #[allow(dead_code)]
316    base_pool: Arc<Mutex<ThreadPoolManager>>,
317    monitoring: Arc<Mutex<ThreadPoolMonitoring>>,
318    scaling_policy: ScalingPolicy,
319    load_balancer: LoadBalancer,
320}
321
322impl EnhancedThreadPool {
323    /// Create a new enhanced thread pool
324    pub fn new(profile: ThreadPoolProfile) -> Self {
325        let base_pool = Arc::new(Mutex::new(ThreadPoolManager::new().with_profile(profile)));
326
327        Self {
328            base_pool,
329            monitoring: Arc::new(Mutex::new(ThreadPoolMonitoring::new())),
330            scaling_policy: ScalingPolicy::Conservative,
331            load_balancer: LoadBalancer::RoundRobin,
332        }
333    }
334
335    /// Set scaling policy
336    pub fn with_scaling_policy(mut self, policy: ScalingPolicy) -> Self {
337        self.scaling_policy = policy;
338        self
339    }
340
341    /// Set load balancing strategy
342    pub fn with_load_balancer(mut self, balancer: LoadBalancer) -> Self {
343        self.load_balancer = balancer;
344        self
345    }
346
347    /// Get current thread pool metrics
348    pub fn get_metrics(&self) -> ThreadPoolMetrics {
349        let monitoring = self.monitoring.lock().expect("Operation failed");
350        monitoring.get_metrics()
351    }
352
353    /// Execute task with monitoring and adaptive scaling
354    pub fn execute_monitored<F, R>(&self, task: F) -> R
355    where
356        F: FnOnce() -> R + Send,
357        R: Send,
358    {
359        let start_time = std::time::Instant::now();
360
361        // Update monitoring before execution
362        {
363            let mut monitoring = self.monitoring.lock().expect("Operation failed");
364            monitoring.record_task_start();
365        }
366
367        // Execute task
368        let result = task();
369
370        // Update monitoring after execution
371        {
372            let mut monitoring = self.monitoring.lock().expect("Operation failed");
373            monitoring.record_task_completion(start_time.elapsed());
374        }
375
376        // Check if scaling is needed
377        self.check_and_scale();
378
379        result
380    }
381
382    /// Check if thread pool scaling is needed and apply if necessary
383    fn check_and_scale(&self) {
384        let metrics = self.get_metrics();
385
386        match self.scaling_policy {
387            ScalingPolicy::Conservative => {
388                // Scale up only if utilization > 90% for extended period
389                if metrics.average_utilization > 0.9 && metrics.queue_length > 10 {
390                    self.scale_up();
391                }
392                // Scale down only if utilization < 30% for extended period
393                else if metrics.average_utilization < 0.3 && metrics.active_threads > 2 {
394                    self.scale_down();
395                }
396            }
397            ScalingPolicy::Aggressive => {
398                // Scale up if utilization > 70%
399                if metrics.average_utilization > 0.7 {
400                    self.scale_up();
401                }
402                // Scale down if utilization < 50%
403                else if metrics.average_utilization < 0.5 && metrics.active_threads > 1 {
404                    self.scale_down();
405                }
406            }
407            ScalingPolicy::LatencyOptimized => {
408                // Prioritize low latency over efficiency
409                if metrics.average_latency_ms > 10.0 {
410                    self.scale_up();
411                } else if metrics.average_latency_ms < 2.0 && metrics.active_threads > 2 {
412                    self.scale_down();
413                }
414            }
415            ScalingPolicy::Fixed => {
416                // No scaling
417            }
418        }
419    }
420
421    /// Scale up the thread pool
422    fn scale_up(&self) {
423        // Implementation would involve creating new threads
424        // For now, we'll just log the intent
425        println!("Scaling up thread pool due to high utilization");
426    }
427
428    /// Scale down the thread pool
429    fn scale_down(&self) {
430        // Implementation would involve reducing threads
431        // For now, we'll just log the intent
432        println!("Scaling down thread pool due to low utilization");
433    }
434}
435
436/// Thread pool scaling policies
437#[derive(Debug, Clone, Copy)]
438pub enum ScalingPolicy {
439    /// Conservative scaling - only scale when definitely needed
440    Conservative,
441    /// Aggressive scaling - scale more readily for performance
442    Aggressive,
443    /// Optimized for low latency
444    LatencyOptimized,
445    /// Fixed thread count - no scaling
446    Fixed,
447}
448
449/// Load balancing strategies
450#[derive(Debug, Clone, Copy)]
451pub enum LoadBalancer {
452    /// Simple round-robin task distribution
453    RoundRobin,
454    /// Least loaded thread gets next task
455    LeastLoaded,
456    /// Work-stealing between threads
457    WorkStealing,
458    /// NUMA-aware task assignment
459    NumaAware,
460}
461
462/// Thread pool monitoring and metrics collection
463struct ThreadPoolMonitoring {
464    task_count: usize,
465    total_execution_time: std::time::Duration,
466    active_threads: usize,
467    queue_length: usize,
468    start_times: Vec<std::time::Instant>,
469}
470
471impl ThreadPoolMonitoring {
472    fn new() -> Self {
473        Self {
474            task_count: 0,
475            total_execution_time: std::time::Duration::ZERO,
476            active_threads: 0,
477            queue_length: 0,
478            start_times: Vec::new(),
479        }
480    }
481
482    fn record_task_start(&mut self) {
483        self.task_count += 1;
484        self.start_times.push(std::time::Instant::now());
485        self.queue_length += 1;
486    }
487
488    fn record_task_completion(&mut self, duration: std::time::Duration) {
489        self.total_execution_time += duration;
490        self.queue_length = self.queue_length.saturating_sub(1);
491    }
492
493    fn get_metrics(&self) -> ThreadPoolMetrics {
494        ThreadPoolMetrics {
495            active_threads: self.active_threads,
496            queue_length: self.queue_length,
497            total_tasks: self.task_count,
498            average_utilization: if self.active_threads > 0 {
499                self.queue_length as f64 / self.active_threads as f64
500            } else {
501                0.0
502            },
503            average_latency_ms: if self.task_count > 0 {
504                self.total_execution_time.as_millis() as f64 / self.task_count as f64
505            } else {
506                0.0
507            },
508            throughput_tasks_per_sec: if !self.total_execution_time.is_zero() {
509                self.task_count as f64 / self.total_execution_time.as_secs_f64()
510            } else {
511                0.0
512            },
513        }
514    }
515}
516
517/// Thread pool performance metrics
518#[derive(Debug, Clone)]
519pub struct ThreadPoolMetrics {
520    /// Number of currently active threads
521    pub active_threads: usize,
522    /// Number of tasks waiting in queue
523    pub queue_length: usize,
524    /// Total number of tasks processed
525    pub total_tasks: usize,
526    /// Average thread utilization (0.0 to 1.0+)
527    pub average_utilization: f64,
528    /// Average task latency in milliseconds
529    pub average_latency_ms: f64,
530    /// Throughput in tasks per second
531    pub throughput_tasks_per_sec: f64,
532}