Skip to main content

scirs2_linalg/parallel/affinity/
mod.rs

1//! CPU affinity and thread pinning
2//!
3//! This module provides advanced thread affinity management for optimal
4//! CPU utilization and NUMA-aware scheduling.
5
6use super::{numa::NumaTopology, WorkerConfig};
7use std::sync::{Arc, Mutex};
8
9/// Thread affinity strategy
10#[derive(Debug, Clone, Copy)]
11pub enum AffinityStrategy {
12    /// No specific affinity - let OS scheduler decide
13    None,
14    /// Pin threads to specific CPU cores
15    Pinned,
16    /// Spread threads across NUMA nodes
17    NumaSpread,
18    /// Compact threads within NUMA nodes
19    NumaCompact,
20    /// Custom affinity mapping
21    Custom,
22}
23
24/// CPU core affinity mask
25#[derive(Debug, Clone)]
26pub struct CoreAffinity {
27    /// CPU core IDs to bind threads to
28    pub core_ids: Vec<usize>,
29    /// Whether to allow thread migration
30    pub allow_migration: bool,
31    /// NUMA node preference
32    pub numa_node: Option<usize>,
33}
34
35impl CoreAffinity {
36    /// Create affinity for specific cores
37    pub fn cores(_coreids: Vec<usize>) -> Self {
38        Self {
39            core_ids: _coreids,
40            allow_migration: false,
41            numa_node: None,
42        }
43    }
44
45    /// Create affinity for a NUMA node
46    pub fn numa_node(_nodeid: usize, topology: &NumaTopology) -> Self {
47        let core_ids = if _nodeid < topology.cpus_per_node.len() {
48            topology.cpus_per_node[_nodeid].clone()
49        } else {
50            vec![]
51        };
52
53        Self {
54            core_ids,
55            allow_migration: true,
56            numa_node: Some(_nodeid),
57        }
58    }
59
60    /// Allow thread migration between specified cores
61    pub fn with_migration(mut self, allow: bool) -> Self {
62        self.allow_migration = allow;
63        self
64    }
65}
66
67/// Thread affinity manager
68pub struct AffinityManager {
69    strategy: AffinityStrategy,
70    topology: NumaTopology,
71    thread_assignments: Arc<Mutex<Vec<Option<CoreAffinity>>>>,
72}
73
74impl AffinityManager {
75    /// Create a new affinity manager
76    pub fn new(strategy: AffinityStrategy, topology: NumaTopology) -> Self {
77        let thread_assignments = Arc::new(Mutex::new(Vec::new()));
78        Self {
79            strategy,
80            topology,
81            thread_assignments,
82        }
83    }
84
85    /// Generate thread affinity assignments based on strategy
86    pub fn generate_assignments(&self, numthreads: usize) -> Vec<CoreAffinity> {
87        match self.strategy {
88            AffinityStrategy::None => {
89                // No specific affinity
90                vec![]
91            }
92            AffinityStrategy::Pinned => self.generate_pinned_assignments(numthreads),
93            AffinityStrategy::NumaSpread => self.generate_numa_spread_assignments(numthreads),
94            AffinityStrategy::NumaCompact => self.generate_numa_compact_assignments(numthreads),
95            AffinityStrategy::Custom => {
96                // Use existing assignments
97                self.thread_assignments
98                    .lock()
99                    .expect("Operation failed")
100                    .iter()
101                    .filter_map(|opt| opt.clone())
102                    .collect()
103            }
104        }
105    }
106
107    /// Generate pinned affinity assignments (one thread per core)
108    fn generate_pinned_assignments(&self, numthreads: usize) -> Vec<CoreAffinity> {
109        let total_cores: usize = self
110            .topology
111            .cpus_per_node
112            .iter()
113            .map(|node| node.len())
114            .sum();
115
116        let effective_threads = std::cmp::min(numthreads, total_cores);
117        let mut all_cores: Vec<usize> = self
118            .topology
119            .cpus_per_node
120            .iter()
121            .flat_map(|node| node.iter().cloned())
122            .collect();
123
124        // Sort cores for consistent assignment
125        all_cores.sort_unstable();
126
127        (0..effective_threads)
128            .map(|i| CoreAffinity::cores(vec![all_cores[i]]))
129            .collect()
130    }
131
132    /// Generate NUMA-spread assignments (distribute across nodes)
133    fn generate_numa_spread_assignments(&self, numthreads: usize) -> Vec<CoreAffinity> {
134        let mut assignments = Vec::new();
135        let threads_per_node = numthreads / self.topology.num_nodes;
136        let extra_threads = numthreads % self.topology.num_nodes;
137
138        for (node_id, cores) in self.topology.cpus_per_node.iter().enumerate() {
139            let node_threads = threads_per_node + if node_id < extra_threads { 1 } else { 0 };
140
141            for i in 0..node_threads {
142                if i < cores.len() {
143                    assignments.push(CoreAffinity::cores(vec![cores[i]]).with_migration(false));
144                } else {
145                    // More _threads than cores in this node - allow migration
146                    assignments.push(
147                        CoreAffinity::numa_node(node_id, &self.topology).with_migration(true),
148                    );
149                }
150            }
151        }
152
153        assignments
154    }
155
156    /// Generate NUMA-compact assignments (fill nodes sequentially)
157    fn generate_numa_compact_assignments(&self, numthreads: usize) -> Vec<CoreAffinity> {
158        let mut assignments = Vec::new();
159        let mut remaining_threads = numthreads;
160
161        for (node_id, cores) in self.topology.cpus_per_node.iter().enumerate() {
162            if remaining_threads == 0 {
163                break;
164            }
165
166            let node_capacity = cores.len();
167            let threads_for_node = std::cmp::min(remaining_threads, node_capacity);
168
169            // Assign _threads to specific cores in this node
170            for core in cores.iter().take(threads_for_node) {
171                assignments.push(CoreAffinity::cores(vec![*core]).with_migration(false));
172            }
173
174            // If more _threads needed than cores, allow migration within node
175            if remaining_threads > node_capacity {
176                for _ in node_capacity..remaining_threads.min(node_capacity * 2) {
177                    assignments.push(
178                        CoreAffinity::numa_node(node_id, &self.topology).with_migration(true),
179                    );
180                }
181            }
182
183            remaining_threads = remaining_threads.saturating_sub(node_capacity * 2);
184        }
185
186        assignments
187    }
188
189    /// Set custom affinity for a specific thread
190    pub fn set_thread_affinity(&self, threadid: usize, affinity: CoreAffinity) {
191        let mut assignments = self.thread_assignments.lock().expect("Operation failed");
192
193        // Expand vector if needed
194        while assignments.len() <= threadid {
195            assignments.push(None);
196        }
197
198        assignments[threadid] = Some(affinity);
199    }
200
201    /// Get affinity for a specific thread
202    pub fn get_thread_affinity(&self, threadid: usize) -> Option<CoreAffinity> {
203        let assignments = self.thread_assignments.lock().expect("Operation failed");
204        assignments.get(threadid).and_then(|opt| opt.clone())
205    }
206
207    /// Apply affinity settings to current thread (platform-specific)
208    pub fn apply_current_thread_affinity(&self, _affinity: &CoreAffinity) -> Result<(), String> {
209        // Note: This is a simplified implementation
210        // In a real implementation, you would use platform-specific APIs:
211        // - On Linux: sched_setaffinity, pthread_setaffinity_np
212        // - On Windows: SetThreadAffinityMask
213        // - On macOS: thread_policy_set
214
215        #[cfg(target_os = "linux")]
216        {
217            self.apply_linux_affinity(_affinity)
218        }
219
220        #[cfg(target_os = "windows")]
221        {
222            self.apply_windows_affinity(_affinity)
223        }
224
225        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
226        {
227            // Fallback for unsupported platforms
228            eprintln!("CPU affinity not supported on this platform");
229            Ok(())
230        }
231    }
232
233    #[cfg(target_os = "linux")]
234    fn apply_linux_affinity(&self, affinity: &CoreAffinity) -> Result<(), String> {
235        // This would typically use libc::sched_setaffinity
236        // For now, we'll just set environment variables that some libraries recognize
237        if !affinity.core_ids.is_empty() {
238            let core_list = affinity
239                .core_ids
240                .iter()
241                .map(|id| id.to_string())
242                .collect::<Vec<_>>()
243                .join(",");
244
245            std::env::set_var("GOMP_CPU_AFFINITY", &core_list);
246            std::env::set_var("KMP_AFFINITY", format!("explicit,proclist=[{core_list}]"));
247        }
248
249        if let Some(numa_node) = affinity.numa_node {
250            std::env::set_var("NUMA_NODE_HINT", numa_node.to_string());
251        }
252
253        Ok(())
254    }
255
256    #[cfg(target_os = "windows")]
257    fn apply_windows_affinity(&self, affinity: &CoreAffinity) -> Result<(), String> {
258        // This would typically use Windows APIs like SetThreadAffinityMask
259        // For now, we'll set environment variables
260        if !affinity.core_ids.is_empty() {
261            let core_mask: u64 = affinity
262                .core_ids
263                .iter()
264                .fold(0u64, |mask, &core_id| mask | (1u64 << core_id));
265
266            std::env::set_var("THREAD_AFFINITY_MASK", format!("0x{:x}", core_mask));
267        }
268
269        Ok(())
270    }
271
272    /// Get optimal affinity strategy for current system
273    pub fn recommend_strategy(
274        &self,
275        num_threads: usize,
276        workload_type: WorkloadType,
277    ) -> AffinityStrategy {
278        match workload_type {
279            WorkloadType::CpuBound => {
280                if num_threads <= self.total_cores() {
281                    AffinityStrategy::Pinned
282                } else {
283                    AffinityStrategy::NumaSpread
284                }
285            }
286            WorkloadType::MemoryBound => {
287                if self.topology.num_nodes > 1 {
288                    AffinityStrategy::NumaCompact
289                } else {
290                    AffinityStrategy::Pinned
291                }
292            }
293            WorkloadType::Balanced => {
294                if self.topology.num_nodes > 1 && num_threads >= self.topology.num_nodes {
295                    AffinityStrategy::NumaSpread
296                } else {
297                    AffinityStrategy::Pinned
298                }
299            }
300            WorkloadType::Latency => AffinityStrategy::Pinned,
301        }
302    }
303
304    /// Get total number of CPU cores
305    fn total_cores(&self) -> usize {
306        self.topology
307            .cpus_per_node
308            .iter()
309            .map(|node| node.len())
310            .sum()
311    }
312}
313
314/// Type of computational workload
315#[derive(Debug, Clone, Copy)]
316pub enum WorkloadType {
317    /// CPU-intensive workload
318    CpuBound,
319    /// Memory-intensive workload
320    MemoryBound,
321    /// Balanced CPU and memory usage
322    Balanced,
323    /// Latency-sensitive workload
324    Latency,
325}
326
327/// Thread pool with affinity support
328pub struct AffinityThreadPool {
329    affinity_manager: AffinityManager,
330    config: WorkerConfig,
331}
332
333impl AffinityThreadPool {
334    /// Create a new affinity-aware thread pool
335    pub fn new(strategy: AffinityStrategy, topology: NumaTopology, config: WorkerConfig) -> Self {
336        let affinity_manager = AffinityManager::new(strategy, topology);
337        Self {
338            affinity_manager,
339            config,
340        }
341    }
342
343    /// Execute work with affinity-pinned threads
344    pub fn execute_with_affinity<F, R>(&self, work: F) -> R
345    where
346        F: FnOnce() -> R + Send,
347        R: Send,
348    {
349        let num_threads = self.config.workers.unwrap_or(
350            std::thread::available_parallelism()
351                .map(|n| n.get())
352                .unwrap_or(4),
353        );
354
355        let assignments = self.affinity_manager.generate_assignments(num_threads);
356
357        // Apply affinity to current thread if assignments available
358        if let Some(affinity) = assignments.first() {
359            if let Err(e) = self
360                .affinity_manager
361                .apply_current_thread_affinity(affinity)
362            {
363                eprintln!("Warning: Failed to set thread affinity: {e}");
364            }
365        }
366
367        // Execute the work
368        work()
369    }
370
371    /// Get affinity information for debugging
372    pub fn get_affinity_info(&self) -> AffinityInfo {
373        let num_threads = self.config.workers.unwrap_or(
374            std::thread::available_parallelism()
375                .map(|n| n.get())
376                .unwrap_or(4),
377        );
378
379        let assignments = self.affinity_manager.generate_assignments(num_threads);
380
381        AffinityInfo {
382            strategy: self.affinity_manager.strategy,
383            num_threads,
384            assignments,
385            topology: self.affinity_manager.topology.clone(),
386        }
387    }
388}
389
390/// Affinity information for debugging and monitoring
391#[derive(Debug, Clone)]
392pub struct AffinityInfo {
393    pub strategy: AffinityStrategy,
394    pub num_threads: usize,
395    pub assignments: Vec<CoreAffinity>,
396    pub topology: NumaTopology,
397}
398
399impl AffinityInfo {
400    /// Print detailed affinity information
401    pub fn print_summary(&self) {
402        println!("=== Thread Affinity Summary ===");
403        println!("Strategy: {:?}", self.strategy);
404        println!("Number of threads: {}", self.num_threads);
405        println!("NUMA nodes: {}", self.topology.num_nodes);
406
407        for (node_id, cores) in self.topology.cpus_per_node.iter().enumerate() {
408            println!("  Node {node_id}: CPUs {cores:?}");
409        }
410
411        println!("Thread assignments:");
412        for (thread_id, affinity) in self.assignments.iter().enumerate() {
413            println!(
414                "  Thread {}: cores {:?}, migration: {}, NUMA: {:?}",
415                thread_id, affinity.core_ids, affinity.allow_migration, affinity.numa_node
416            );
417        }
418        println!("==============================");
419    }
420
421    /// Get affinity efficiency metrics
422    pub fn efficiency_metrics(&self) -> AffinityEfficiencyMetrics {
423        let cores_used: std::collections::HashSet<usize> = self
424            .assignments
425            .iter()
426            .flat_map(|affinity| affinity.core_ids.iter().cloned())
427            .collect();
428
429        let total_cores: usize = self
430            .topology
431            .cpus_per_node
432            .iter()
433            .map(|node| node.len())
434            .sum();
435
436        let numa_nodes_used: std::collections::HashSet<usize> = self
437            .assignments
438            .iter()
439            .filter_map(|affinity| affinity.numa_node)
440            .collect();
441
442        let threads_with_migration: usize = self
443            .assignments
444            .iter()
445            .filter(|affinity| affinity.allow_migration)
446            .count();
447
448        AffinityEfficiencyMetrics {
449            core_utilization: cores_used.len() as f64 / total_cores as f64,
450            numa_spread: numa_nodes_used.len() as f64 / self.topology.num_nodes as f64,
451            migration_ratio: threads_with_migration as f64 / self.num_threads as f64,
452            threads_per_core: self.num_threads as f64 / cores_used.len() as f64,
453        }
454    }
455}
456
457/// Metrics for evaluating affinity efficiency
458#[derive(Debug, Clone)]
459pub struct AffinityEfficiencyMetrics {
460    /// Fraction of CPU cores being used (0.0 to 1.0)
461    pub core_utilization: f64,
462    /// Fraction of NUMA nodes being used (0.0 to 1.0)
463    pub numa_spread: f64,
464    /// Fraction of threads that allow migration (0.0 to 1.0)
465    pub migration_ratio: f64,
466    /// Average number of threads per CPU core
467    pub threads_per_core: f64,
468}
469
470/// Utility functions for affinity management
471pub mod utils {
472    use super::*;
473
474    /// Auto-detect optimal affinity strategy for a workload
475    pub fn auto_detect_strategy(
476        workload_type: WorkloadType,
477        num_threads: usize,
478        topology: &NumaTopology,
479    ) -> AffinityStrategy {
480        let manager = AffinityManager::new(AffinityStrategy::None, topology.clone());
481        manager.recommend_strategy(num_threads, workload_type)
482    }
483
484    /// Create optimized thread pool for matrix operations
485    pub fn creatematrix_thread_pool(
486        matrixsize: (usize, usize),
487        topology: NumaTopology,
488    ) -> AffinityThreadPool {
489        let workload_type = if matrixsize.0 * matrixsize.1 > 1_000_000 {
490            WorkloadType::MemoryBound
491        } else {
492            WorkloadType::CpuBound
493        };
494
495        let num_threads = std::cmp::min(
496            topology.cpus_per_node.iter().map(|node| node.len()).sum(),
497            std::thread::available_parallelism()
498                .map(|n| n.get())
499                .unwrap_or(4),
500        );
501
502        let strategy = auto_detect_strategy(workload_type, num_threads, &topology);
503        let config = WorkerConfig::new().with_workers(num_threads);
504
505        AffinityThreadPool::new(strategy, topology, config)
506    }
507
508    /// Benchmark different affinity strategies
509    pub fn benchmark_affinity_strategies<F>(
510        workload: F,
511        topology: NumaTopology,
512        config: WorkerConfig,
513    ) -> Vec<(AffinityStrategy, f64)>
514    where
515        F: Fn() -> f64 + Clone + Send + Sync,
516    {
517        let strategies = vec![
518            AffinityStrategy::None,
519            AffinityStrategy::Pinned,
520            AffinityStrategy::NumaSpread,
521            AffinityStrategy::NumaCompact,
522        ];
523
524        let mut results = Vec::new();
525
526        for strategy in strategies {
527            let pool = AffinityThreadPool::new(strategy, topology.clone(), config.clone());
528
529            // Warm up
530            for _ in 0..3 {
531                pool.execute_with_affinity(&workload);
532            }
533
534            // Benchmark
535            let start = std::time::Instant::now();
536            let iterations = 10;
537            let mut total_work = 0.0;
538
539            for _ in 0..iterations {
540                total_work += pool.execute_with_affinity(&workload);
541            }
542
543            let elapsed = start.elapsed().as_secs_f64();
544            let throughput = total_work / elapsed;
545
546            results.push((strategy, throughput));
547        }
548
549        results
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556
557    #[test]
558    fn test_core_affinity_creation() {
559        let affinity = CoreAffinity::cores(vec![0, 1, 2]);
560        assert_eq!(affinity.core_ids, vec![0, 1, 2]);
561        assert!(!affinity.allow_migration);
562        assert_eq!(affinity.numa_node, None);
563    }
564
565    #[test]
566    fn test_numa_affinity_creation() {
567        let topology = NumaTopology {
568            num_nodes: 2,
569            cpus_per_node: vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]],
570            memory_bandwidth: vec![vec![100.0, 50.0], vec![50.0, 100.0]],
571        };
572
573        let affinity = CoreAffinity::numa_node(1, &topology);
574        assert_eq!(affinity.core_ids, vec![4, 5, 6, 7]);
575        assert!(affinity.allow_migration);
576        assert_eq!(affinity.numa_node, Some(1));
577    }
578
579    #[test]
580    fn test_affinity_strategy_recommendation() {
581        let topology = NumaTopology {
582            num_nodes: 2,
583            cpus_per_node: vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]],
584            memory_bandwidth: vec![vec![100.0, 50.0], vec![50.0, 100.0]],
585        };
586
587        let manager = AffinityManager::new(AffinityStrategy::None, topology);
588
589        // CPU-bound with few threads should be pinned
590        assert!(matches!(
591            manager.recommend_strategy(4, WorkloadType::CpuBound),
592            AffinityStrategy::Pinned
593        ));
594
595        // Memory-bound should prefer NUMA compact
596        assert!(matches!(
597            manager.recommend_strategy(4, WorkloadType::MemoryBound),
598            AffinityStrategy::NumaCompact
599        ));
600    }
601
602    #[test]
603    fn test_pinned_assignments() {
604        let topology = NumaTopology {
605            num_nodes: 2,
606            cpus_per_node: vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]],
607            memory_bandwidth: vec![vec![100.0, 50.0], vec![50.0, 100.0]],
608        };
609
610        let manager = AffinityManager::new(AffinityStrategy::Pinned, topology);
611        let assignments = manager.generate_assignments(4);
612
613        assert_eq!(assignments.len(), 4);
614        for (i, assignment) in assignments.iter().enumerate() {
615            assert_eq!(assignment.core_ids.len(), 1);
616            assert_eq!(assignment.core_ids[0], i);
617            assert!(!assignment.allow_migration);
618        }
619    }
620
621    #[test]
622    fn test_numa_spread_assignments() {
623        let topology = NumaTopology {
624            num_nodes: 2,
625            cpus_per_node: vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]],
626            memory_bandwidth: vec![vec![100.0, 50.0], vec![50.0, 100.0]],
627        };
628
629        let manager = AffinityManager::new(AffinityStrategy::NumaSpread, topology);
630        let assignments = manager.generate_assignments(4);
631
632        assert_eq!(assignments.len(), 4);
633
634        // Should have 2 threads per NUMA node
635        let node0_threads = assignments
636            .iter()
637            .filter(|a| {
638                a.core_ids.contains(&0)
639                    || a.core_ids.contains(&1)
640                    || a.core_ids.contains(&2)
641                    || a.core_ids.contains(&3)
642            })
643            .count();
644        let node1_threads = assignments
645            .iter()
646            .filter(|a| {
647                a.core_ids.contains(&4)
648                    || a.core_ids.contains(&5)
649                    || a.core_ids.contains(&6)
650                    || a.core_ids.contains(&7)
651            })
652            .count();
653
654        assert_eq!(node0_threads, 2);
655        assert_eq!(node1_threads, 2);
656    }
657}