1use std::cell::UnsafeCell;
31use std::sync::Arc;
32use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
33
34pub static FORCED_THREADS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
38
39pub static WORKER_TIDS: std::sync::Mutex<Vec<i32>> = std::sync::Mutex::new(Vec::new());
43
44
45#[derive(Clone, Copy)]
49struct TaskPtr(*const (dyn Fn(usize, usize) + Sync));
50unsafe impl Send for TaskPtr {}
51
52struct Inner {
53 epoch: AtomicUsize,
55 remaining: AtomicUsize,
57 slot: UnsafeCell<Option<(TaskPtr, usize)>>,
61 shutdown: AtomicBool,
62 spin_budget: usize,
64 parked: Box<[AtomicBool]>,
67}
68
69unsafe impl Sync for Inner {}
73
74static DISPATCHES: AtomicUsize = AtomicUsize::new(0);
77
78pub fn dispatch_count() -> usize {
80 DISPATCHES.load(Ordering::Relaxed)
81}
82
83pub struct Pool {
86 inner: Arc<Inner>,
87 threads: Vec<std::thread::Thread>,
89 joins: Vec<std::thread::JoinHandle<()>>,
90}
91
92fn spin_budget_from_env() -> usize {
93 std::env::var("CMF_POOL_SPIN")
94 .ok()
95 .and_then(|v| v.parse::<usize>().ok())
96 .unwrap_or(4000)
97}
98
99impl Pool {
100 pub fn new(n_workers: usize) -> Self {
101 Self::with_spin(n_workers, spin_budget_from_env())
102 }
103
104 pub fn with_spin(n_workers: usize, spin_budget: usize) -> Self {
106 let inner = Arc::new(Inner {
107 epoch: AtomicUsize::new(0),
108 remaining: AtomicUsize::new(0),
109 slot: UnsafeCell::new(None),
110 shutdown: AtomicBool::new(false),
111 spin_budget,
112 parked: (0..n_workers).map(|_| AtomicBool::new(false)).collect(),
113 });
114 let mut joins = Vec::with_capacity(n_workers);
115 if let Ok(mut tids) = WORKER_TIDS.lock() {
116 tids.clear();
117 }
118 for w in 0..n_workers {
119 let inner = inner.clone();
120 let h = std::thread::Builder::new()
121 .name(format!("cmf-pool-{w}"))
122 .spawn(move || {
123 #[cfg(any(target_os = "android", target_os = "linux"))]
124 if let Ok(mut tids) = WORKER_TIDS.lock() {
125 tids.push(unsafe { libc::gettid() } as i32);
126 }
127 worker_loop(&inner, w)
128 })
129 .expect("spawn pool worker");
130 joins.push(h);
131 }
132 #[cfg(any(target_os = "android", target_os = "linux"))]
139 while WORKER_TIDS.lock().map(|t| t.len()).unwrap_or(n_workers) < n_workers {
140 std::thread::yield_now();
141 }
142 let threads = joins.iter().map(|h| h.thread().clone()).collect();
143 Self {
144 inner,
145 threads,
146 joins,
147 }
148 }
149
150 #[cfg(all(
156 target_arch = "aarch64",
157 any(target_os = "linux", target_os = "android")
158 ))]
159 fn big_cores() -> Option<usize> {
160 Self::cores_from_capacities(&core_capacities())
161 }
162
163 #[cfg_attr(
173 not(all(
174 target_arch = "aarch64",
175 any(target_os = "linux", target_os = "android")
176 )),
177 allow(dead_code)
178 )]
179 fn cores_from_capacities(caps: &[u64]) -> Option<usize> {
180 let max = *caps.iter().max()?;
181 let min = *caps.iter().min()?;
182 if caps.len() < 2 || max == min {
183 return None;
184 }
185 Some(caps.iter().filter(|&&c| c * 8 >= max * 5).count())
186 }
187
188 #[cfg(target_os = "macos")]
189 fn big_cores() -> Option<usize> {
190 unsafe extern "C" {
191 fn sysctlbyname(
192 name: *const std::ffi::c_char,
193 oldp: *mut std::ffi::c_void,
194 oldlenp: *mut usize,
195 newp: *mut std::ffi::c_void,
196 newlen: usize,
197 ) -> std::ffi::c_int;
198 }
199 unsafe {
200 let name = std::ffi::CString::new("hw.perflevel0.physicalcpu").ok()?;
201 let mut count: i32 = 0;
202 let mut size = std::mem::size_of::<i32>();
203 let ret = sysctlbyname(
204 name.as_ptr(),
205 &mut count as *mut i32 as *mut std::ffi::c_void,
206 &mut size,
207 std::ptr::null_mut(),
208 0,
209 );
210 if ret == 0 && count > 0 {
211 Some(count as usize)
212 } else {
213 None
214 }
215 }
216 }
217
218 #[cfg(not(any(
219 all(
220 target_arch = "aarch64",
221 any(target_os = "linux", target_os = "android")
222 ),
223 target_os = "macos"
224 )))]
225 fn big_cores() -> Option<usize> {
226 None
227 }
228
229 pub fn effective_threads() -> usize {
235 let forced = FORCED_THREADS.load(std::sync::atomic::Ordering::Relaxed);
236 if forced > 0 {
237 return forced;
238 }
239 match std::env::var("CMF_THREADS") {
240 Ok(v) => v.parse::<usize>().unwrap_or(0),
241 Err(_) => match Self::big_cores() {
242 Some(big) => big,
243 None => {
244 let avail = std::thread::available_parallelism()
254 .map(|n| n.get())
255 .unwrap_or(1);
256 avail.saturating_sub(1).min(32)
257 }
258 },
259 }
260 }
261
262 pub fn from_env() -> Option<Arc<Self>> {
265 let n = Self::effective_threads();
266 if n <= 1 {
267 None
268 } else {
269 Some(Arc::new(Self::new(n)))
270 }
271 }
272
273 pub fn n_workers(&self) -> usize {
275 self.threads.len()
276 }
277
278 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
289 let grain = (rows / ((self.threads.len() + 1) * 8)).max(32);
292 let next = AtomicUsize::new(0);
293 self.run(&|_w, _n| loop {
294 let start = next.fetch_add(grain, Ordering::Relaxed);
295 if start >= rows {
296 break;
297 }
298 f(start, (start + grain).min(rows));
299 });
300 }
301
302 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
310 let total: usize = parts.iter().map(|p| p.0).sum();
311 if total == 0 {
312 return;
313 }
314 let grain = (total / ((self.threads.len() + 1) * 8)).max(32);
315 let next = AtomicUsize::new(0);
316 self.run(&|_w, _n| loop {
317 let s = next.fetch_add(grain, Ordering::Relaxed);
318 if s >= total {
319 break;
320 }
321 let e = (s + grain).min(total);
322 let mut base = 0usize;
323 for &(rows, f) in parts {
324 let a = s.max(base);
325 let b = e.min(base + rows);
326 if a < b {
327 f(a - base, b - base);
328 }
329 base += rows;
330 if base >= e {
331 break;
332 }
333 }
334 });
335 }
336
337 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
341 DISPATCHES.fetch_add(1, Ordering::Relaxed);
342 let nw = self.threads.len();
343 let n = nw + 1; let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
347 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
348 unsafe { std::mem::transmute(ptr) };
349 unsafe { *self.inner.slot.get() = Some((TaskPtr(ptr), n)) };
352 self.inner.remaining.store(nw, Ordering::Relaxed);
353 self.inner.epoch.fetch_add(1, Ordering::SeqCst);
354 for (i, t) in self.threads.iter().enumerate() {
355 if self.inner.parked[i].load(Ordering::SeqCst) {
356 t.unpark();
357 }
358 }
359
360 f(nw, n);
363
364 let mut spins = 0usize;
366 while self.inner.remaining.load(Ordering::Acquire) != 0 {
367 spins += 1;
368 if spins < 10_000 {
369 std::hint::spin_loop();
370 } else {
371 std::thread::yield_now();
372 }
373 }
374 }
375}
376
377impl Drop for Pool {
378 fn drop(&mut self) {
379 self.inner.shutdown.store(true, Ordering::SeqCst);
380 for t in &self.threads {
381 t.unpark();
382 }
383 for h in self.joins.drain(..) {
384 let _ = h.join();
385 }
386 }
387}
388
389#[cfg(any(
394 target_os = "android",
395 all(target_arch = "aarch64", target_os = "linux")
396))]
397fn core_capacities() -> Vec<u64> {
398 let read_all = |leaf: &str| -> Vec<u64> {
399 let mut vals = Vec::new();
400 for cpu in 0.. {
401 let path = format!("/sys/devices/system/cpu/cpu{cpu}/{leaf}");
402 match std::fs::read_to_string(&path) {
403 Ok(v) => match v.trim().parse() {
404 Ok(x) => vals.push(x),
405 Err(_) => break,
406 },
407 Err(_) => break,
408 }
409 }
410 vals
411 };
412 let caps = read_all("cpu_capacity");
413 if caps.len() >= 2 {
414 return caps;
415 }
416 read_all("cpufreq/cpuinfo_max_freq")
417}
418
419#[cfg(target_os = "android")]
420fn pin_thread_to_big_cores() {
421 use std::mem;
422 let caps = core_capacities();
423 let max = caps.iter().copied().max().unwrap_or(0);
424 let min = caps.iter().copied().min().unwrap_or(0);
425
426 if caps.len() < 2 || max == min {
428 return;
429 }
430
431 unsafe {
432 let mut set: libc::cpu_set_t = mem::zeroed();
433 for (i, &c) in caps.iter().enumerate() {
434 if c * 8 >= max * 5 {
435 libc::CPU_SET(i, &mut set);
436 }
437 }
438 libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &set);
439 }
440}
441
442fn worker_loop(inner: &Inner, idx: usize) {
443 #[cfg(target_os = "android")]
444 pin_thread_to_big_cores();
445
446 let mut seen = 0usize;
451 loop {
452 let mut spins = 0usize;
455 loop {
456 let e = inner.epoch.load(Ordering::Acquire);
457 if e != seen {
458 seen = e;
459 break;
460 }
461 if inner.shutdown.load(Ordering::Relaxed) {
462 return;
463 }
464 if spins < inner.spin_budget {
465 spins += 1;
466 std::hint::spin_loop();
467 } else {
468 inner.parked[idx].store(true, Ordering::SeqCst);
469 if inner.epoch.load(Ordering::SeqCst) == seen
474 && !inner.shutdown.load(Ordering::Relaxed)
475 {
476 std::thread::park();
477 }
478 inner.parked[idx].store(false, Ordering::SeqCst);
479 }
480 }
481 let (task, n) = unsafe { (*inner.slot.get()).expect("job published with epoch") };
485 let f = unsafe { &*task.0 };
486 f(idx, n);
487 inner.remaining.fetch_sub(1, Ordering::AcqRel);
488 }
489}
490
491pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
494 let in_dim = x.len();
495 let out_dim = out.len();
496 debug_assert!(w.len() >= out_dim * in_dim);
497
498 let row_dot = |o: usize| -> f32 {
499 let row = &w[o * in_dim..(o + 1) * in_dim];
500 let mut sum = 0.0f32;
501 for j in 0..in_dim {
502 sum += row[j] * x[j];
503 }
504 sum
505 };
506
507 match pool {
508 Some(pool) if out_dim >= 256 => {
509 let out_addr = SendMut(out.as_mut_ptr());
510 let run_range = move |start: usize, end: usize| {
511 for o in start..end {
512 unsafe { *out_addr.at(o) = row_dot(o) };
513 }
514 };
515 pool.run_rows(out_dim, &run_range);
516 }
517 _ => {
518 for (o, dst) in out.iter_mut().enumerate() {
519 *dst = row_dot(o);
520 }
521 }
522 }
523}
524
525pub fn matvec_rows2(
531 pool: Option<&Pool>,
532 w: &[f32],
533 x1: &[f32],
534 x2: &[f32],
535 out1: &mut [f32],
536 out2: &mut [f32],
537) {
538 let in_dim = x1.len();
539 debug_assert_eq!(x2.len(), in_dim);
540 let out_dim = out1.len();
541 debug_assert_eq!(out2.len(), out_dim);
542 debug_assert!(w.len() >= out_dim * in_dim);
543
544 let row_dots = |o: usize| -> (f32, f32) {
545 let row = &w[o * in_dim..(o + 1) * in_dim];
546 let (mut s1, mut s2) = (0.0f32, 0.0f32);
547 for j in 0..in_dim {
548 s1 += row[j] * x1[j];
549 s2 += row[j] * x2[j];
550 }
551 (s1, s2)
552 };
553
554 match pool {
555 Some(pool) if out_dim >= 256 => {
556 let o1 = SendMut(out1.as_mut_ptr());
557 let o2 = SendMut(out2.as_mut_ptr());
558 let run_range = move |start: usize, end: usize| {
559 for o in start..end {
560 let (s1, s2) = row_dots(o);
561 unsafe {
562 *o1.at(o) = s1;
563 *o2.at(o) = s2;
564 }
565 }
566 };
567 pool.run_rows(out_dim, &run_range);
568 }
569 _ => {
570 for o in 0..out_dim {
571 let (s1, s2) = row_dots(o);
572 out1[o] = s1;
573 out2[o] = s2;
574 }
575 }
576 }
577}
578
579#[derive(Clone, Copy)]
580struct SendMut(*mut f32);
581unsafe impl Send for SendMut {}
582unsafe impl Sync for SendMut {}
583
584impl SendMut {
585 #[inline]
588 fn at(self, i: usize) -> *mut f32 {
589 unsafe { self.0.add(i) }
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 #[test]
596 #[cfg(any(target_os = "android", target_os = "linux"))]
597 fn worker_tids_registered_before_new_returns() {
598 let _p = super::Pool::new(3);
599 assert_eq!(
600 super::WORKER_TIDS.lock().unwrap().len(),
601 3,
602 "all worker tids must be visible the moment the pool exists"
603 );
604 }
605
606 #[test]
607 fn forced_threads_overrides_env_and_topology() {
608 use std::sync::atomic::Ordering;
609 super::FORCED_THREADS.store(3, Ordering::Relaxed);
610 let pool = super::Pool::from_env().expect("forced 3 → pool");
611 assert_eq!(pool.n_workers(), 3);
612 super::FORCED_THREADS.store(1, Ordering::Relaxed);
613 assert!(super::Pool::from_env().is_none(), "forced 1 → serial");
614 super::FORCED_THREADS.store(0, Ordering::Relaxed);
615 }
616
617 #[test]
618 fn capacity_split_clock_bins_vs_microarch() {
619 type P = super::Pool;
620 assert_eq!(
622 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 768, 768, 768, 768]),
623 Some(8)
624 );
625 assert_eq!(
627 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 350, 350, 350, 350]),
628 Some(4)
629 );
630 assert_eq!(
632 P::cores_from_capacities(&[1024, 800, 800, 800, 800, 300, 300, 300]),
633 Some(5)
634 );
635 assert_eq!(P::cores_from_capacities(&[1024; 8]), None);
637 assert_eq!(P::cores_from_capacities(&[]), None);
638 }
639
640 use super::*;
641
642 #[test]
643 fn parallel_matvec_equals_serial_bitexact() {
644 let (out_dim, in_dim) = (512, 64);
645 let w: Vec<f32> = (0..out_dim * in_dim)
646 .map(|i| (i as f32 * 0.013).sin())
647 .collect();
648 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
649
650 let mut serial = vec![0.0f32; out_dim];
651 matvec_rows(None, &w, &x, &mut serial);
652
653 let pool = Pool::new(4);
654 let mut parallel = vec![0.0f32; out_dim];
655 matvec_rows(Some(&pool), &w, &x, &mut parallel);
656
657 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
658 }
659
660 #[test]
661 fn fused_pair_equals_two_singles_bitexact() {
662 let (out_dim, in_dim) = (300, 48);
663 let w: Vec<f32> = (0..out_dim * in_dim)
664 .map(|i| (i as f32 * 0.011).sin())
665 .collect();
666 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
667 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
668
669 let mut a1 = vec![0.0f32; out_dim];
670 let mut a2 = vec![0.0f32; out_dim];
671 matvec_rows(None, &w, &x1, &mut a1);
672 matvec_rows(None, &w, &x2, &mut a2);
673
674 for pool in [None, Some(Pool::new(3))] {
675 let mut b1 = vec![0.0f32; out_dim];
676 let mut b2 = vec![0.0f32; out_dim];
677 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
678 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
679 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
680 }
681 }
682
683 #[test]
684 fn pool_survives_many_runs() {
685 let pool = Pool::new(3);
686 let counter = AtomicUsize::new(0);
687 for _ in 0..100 {
688 pool.run(&|_, _| {
689 counter.fetch_add(1, Ordering::Relaxed);
690 });
691 }
692 assert_eq!(counter.load(Ordering::Relaxed), 400);
694 }
695
696 #[test]
697 fn pool_wakes_after_park() {
698 let pool = Pool::with_spin(2, 0);
701 let counter = AtomicUsize::new(0);
702 for _ in 0..50 {
703 pool.run(&|_, _| {
704 counter.fetch_add(1, Ordering::Relaxed);
705 });
706 std::thread::sleep(std::time::Duration::from_micros(200));
708 }
709 assert_eq!(counter.load(Ordering::Relaxed), 150);
710 }
711
712 #[test]
713 fn worker_indices_are_distinct_and_cover_range() {
714 let pool = Pool::new(3);
715 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
716 for _ in 0..20 {
717 pool.run(&|widx, n| {
718 assert_eq!(n, 4);
719 hits[widx].fetch_add(1, Ordering::Relaxed);
720 });
721 }
722 for (i, h) in hits.iter().enumerate() {
723 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
724 }
725 }
726}