1use super::{numa::NumaTopology, WorkerConfig};
7use std::sync::{Arc, Mutex};
8
9#[derive(Debug, Clone, Copy)]
11pub enum AffinityStrategy {
12 None,
14 Pinned,
16 NumaSpread,
18 NumaCompact,
20 Custom,
22}
23
24#[derive(Debug, Clone)]
26pub struct CoreAffinity {
27 pub core_ids: Vec<usize>,
29 pub allow_migration: bool,
31 pub numa_node: Option<usize>,
33}
34
35impl CoreAffinity {
36 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 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 pub fn with_migration(mut self, allow: bool) -> Self {
62 self.allow_migration = allow;
63 self
64 }
65}
66
67pub struct AffinityManager {
69 strategy: AffinityStrategy,
70 topology: NumaTopology,
71 thread_assignments: Arc<Mutex<Vec<Option<CoreAffinity>>>>,
72}
73
74impl AffinityManager {
75 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 pub fn generate_assignments(&self, numthreads: usize) -> Vec<CoreAffinity> {
87 match self.strategy {
88 AffinityStrategy::None => {
89 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 self.thread_assignments
98 .lock()
99 .expect("Operation failed")
100 .iter()
101 .filter_map(|opt| opt.clone())
102 .collect()
103 }
104 }
105 }
106
107 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 all_cores.sort_unstable();
126
127 (0..effective_threads)
128 .map(|i| CoreAffinity::cores(vec![all_cores[i]]))
129 .collect()
130 }
131
132 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 assignments.push(
147 CoreAffinity::numa_node(node_id, &self.topology).with_migration(true),
148 );
149 }
150 }
151 }
152
153 assignments
154 }
155
156 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 for core in cores.iter().take(threads_for_node) {
171 assignments.push(CoreAffinity::cores(vec![*core]).with_migration(false));
172 }
173
174 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 pub fn set_thread_affinity(&self, threadid: usize, affinity: CoreAffinity) {
191 let mut assignments = self.thread_assignments.lock().expect("Operation failed");
192
193 while assignments.len() <= threadid {
195 assignments.push(None);
196 }
197
198 assignments[threadid] = Some(affinity);
199 }
200
201 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 pub fn apply_current_thread_affinity(&self, _affinity: &CoreAffinity) -> Result<(), String> {
209 #[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 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 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 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 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 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#[derive(Debug, Clone, Copy)]
316pub enum WorkloadType {
317 CpuBound,
319 MemoryBound,
321 Balanced,
323 Latency,
325}
326
327pub struct AffinityThreadPool {
329 affinity_manager: AffinityManager,
330 config: WorkerConfig,
331}
332
333impl AffinityThreadPool {
334 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 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 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 work()
369 }
370
371 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#[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 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 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#[derive(Debug, Clone)]
459pub struct AffinityEfficiencyMetrics {
460 pub core_utilization: f64,
462 pub numa_spread: f64,
464 pub migration_ratio: f64,
466 pub threads_per_core: f64,
468}
469
470pub mod utils {
472 use super::*;
473
474 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 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 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 for _ in 0..3 {
531 pool.execute_with_affinity(&workload);
532 }
533
534 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 assert!(matches!(
591 manager.recommend_strategy(4, WorkloadType::CpuBound),
592 AffinityStrategy::Pinned
593 ));
594
595 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 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}