scirs2_linalg/parallel/thread_pool/
mod.rs1use super::configure_workers;
7use scirs2_core::parallel_ops::*;
8use std::sync::{Arc, Mutex, Once};
9
10static INIT: Once = Once::new();
12static mut GLOBAL_POOL: Option<Arc<Mutex<ThreadPoolManager>>> = None;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ThreadPoolProfile {
17 Default,
19 CpuBound,
21 MemoryBound,
23 LatencySensitive,
25 Custom(usize),
27}
28
29impl ThreadPoolProfile {
30 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 std::thread::available_parallelism()
42 .map(|n| std::cmp::max(1, n.get() / 2))
43 .unwrap_or(2)
44 }
45 ThreadPoolProfile::LatencySensitive => {
46 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
56pub struct ThreadPoolManager {
58 profile: ThreadPoolProfile,
59 stacksize: Option<usize>,
61 thread_name_prefix: String,
63 cpu_affinity: bool,
65}
66
67impl ThreadPoolManager {
68 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 pub fn with_profile(mut self, profile: ThreadPoolProfile) -> Self {
80 self.profile = profile;
81 self
82 }
83
84 pub fn with_stacksize(mut self, size: usize) -> Self {
86 self.stacksize = Some(size);
87 self
88 }
89
90 pub fn with_thread_name_prefix(mut self, prefix: String) -> Self {
92 self.thread_name_prefix = prefix;
93 self
94 }
95
96 pub fn with_cpu_affinity(mut self, enabled: bool) -> Self {
98 self.cpu_affinity = enabled;
99 self
100 }
101
102 pub fn initialize(&self) -> Result<(), String> {
104 let num_threads = self.profile.num_threads();
105
106 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 std::env::set_var("OMP_NUM_THREADS", num_threads.to_string());
122
123 std::env::set_var("MKL_NUM_THREADS", num_threads.to_string());
125
126 Ok(())
127 }
128
129 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#[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
155pub 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
166pub 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
174pub struct AdaptiveThreadPool {
176 min_threads: usize,
177 max_threads: usize,
178 current_threads: Arc<Mutex<usize>>,
179 cpu_utilization: Arc<Mutex<f64>>,
181}
182
183impl AdaptiveThreadPool {
184 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 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 *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 *current = std::cmp::max(*current - 1, self.min_threads);
211 self.apply_thread_count(*current);
212 }
213 }
214
215 fn apply_thread_count(&self, count: usize) {
217 configure_workers(Some(count));
218 }
219
220 pub fn current_thread_count(&self) -> usize {
222 *self.current_threads.lock().expect("Operation failed")
223 }
224}
225
226pub mod benchmark {
228 use super::*;
229 use std::time::{Duration, Instant};
230
231 #[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 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 if let Err(e) = initialize_global_pool(profile) {
253 eprintln!("Failed to initialize pool for {profile:?}: {e}");
254 continue;
255 }
256
257 for _ in 0..3 {
259 workload();
260 }
261
262 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 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
310pub 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 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 pub fn with_scaling_policy(mut self, policy: ScalingPolicy) -> Self {
337 self.scaling_policy = policy;
338 self
339 }
340
341 pub fn with_load_balancer(mut self, balancer: LoadBalancer) -> Self {
343 self.load_balancer = balancer;
344 self
345 }
346
347 pub fn get_metrics(&self) -> ThreadPoolMetrics {
349 let monitoring = self.monitoring.lock().expect("Operation failed");
350 monitoring.get_metrics()
351 }
352
353 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 {
363 let mut monitoring = self.monitoring.lock().expect("Operation failed");
364 monitoring.record_task_start();
365 }
366
367 let result = task();
369
370 {
372 let mut monitoring = self.monitoring.lock().expect("Operation failed");
373 monitoring.record_task_completion(start_time.elapsed());
374 }
375
376 self.check_and_scale();
378
379 result
380 }
381
382 fn check_and_scale(&self) {
384 let metrics = self.get_metrics();
385
386 match self.scaling_policy {
387 ScalingPolicy::Conservative => {
388 if metrics.average_utilization > 0.9 && metrics.queue_length > 10 {
390 self.scale_up();
391 }
392 else if metrics.average_utilization < 0.3 && metrics.active_threads > 2 {
394 self.scale_down();
395 }
396 }
397 ScalingPolicy::Aggressive => {
398 if metrics.average_utilization > 0.7 {
400 self.scale_up();
401 }
402 else if metrics.average_utilization < 0.5 && metrics.active_threads > 1 {
404 self.scale_down();
405 }
406 }
407 ScalingPolicy::LatencyOptimized => {
408 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 }
418 }
419 }
420
421 fn scale_up(&self) {
423 println!("Scaling up thread pool due to high utilization");
426 }
427
428 fn scale_down(&self) {
430 println!("Scaling down thread pool due to low utilization");
433 }
434}
435
436#[derive(Debug, Clone, Copy)]
438pub enum ScalingPolicy {
439 Conservative,
441 Aggressive,
443 LatencyOptimized,
445 Fixed,
447}
448
449#[derive(Debug, Clone, Copy)]
451pub enum LoadBalancer {
452 RoundRobin,
454 LeastLoaded,
456 WorkStealing,
458 NumaAware,
460}
461
462struct 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#[derive(Debug, Clone)]
519pub struct ThreadPoolMetrics {
520 pub active_threads: usize,
522 pub queue_length: usize,
524 pub total_tasks: usize,
526 pub average_utilization: f64,
528 pub average_latency_ms: f64,
530 pub throughput_tasks_per_sec: f64,
532}