1use std::cell::UnsafeCell;
31use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
32use std::sync::Arc;
33
34#[derive(Clone, Copy)]
38struct TaskPtr(*const (dyn Fn(usize, usize) + Sync));
39unsafe impl Send for TaskPtr {}
40
41struct Inner {
42 epoch: AtomicUsize,
44 remaining: AtomicUsize,
46 slot: UnsafeCell<Option<(TaskPtr, usize)>>,
50 shutdown: AtomicBool,
51 spin_budget: usize,
53 parked: Box<[AtomicBool]>,
56}
57
58unsafe impl Sync for Inner {}
62
63static DISPATCHES: AtomicUsize = AtomicUsize::new(0);
66
67pub fn dispatch_count() -> usize {
69 DISPATCHES.load(Ordering::Relaxed)
70}
71
72pub struct Pool {
75 inner: Arc<Inner>,
76 threads: Vec<std::thread::Thread>,
78 joins: Vec<std::thread::JoinHandle<()>>,
79}
80
81fn spin_budget_from_env() -> usize {
82 std::env::var("CMF_POOL_SPIN")
83 .ok()
84 .and_then(|v| v.parse::<usize>().ok())
85 .unwrap_or(4000)
86}
87
88impl Pool {
89 pub fn new(n_workers: usize) -> Self {
90 Self::with_spin(n_workers, spin_budget_from_env())
91 }
92
93 pub fn with_spin(n_workers: usize, spin_budget: usize) -> Self {
95 let inner = Arc::new(Inner {
96 epoch: AtomicUsize::new(0),
97 remaining: AtomicUsize::new(0),
98 slot: UnsafeCell::new(None),
99 shutdown: AtomicBool::new(false),
100 spin_budget,
101 parked: (0..n_workers).map(|_| AtomicBool::new(false)).collect(),
102 });
103 let mut joins = Vec::with_capacity(n_workers);
104 for w in 0..n_workers {
105 let inner = inner.clone();
106 let h = std::thread::Builder::new()
107 .name(format!("cmf-pool-{w}"))
108 .spawn(move || worker_loop(&inner, w))
109 .expect("spawn pool worker");
110 joins.push(h);
111 }
112 let threads = joins.iter().map(|h| h.thread().clone()).collect();
113 Self { inner, threads, joins }
114 }
115
116 #[cfg(all(target_arch = "aarch64", any(target_os = "linux", target_os = "android")))]
122 fn big_cores() -> Option<usize> {
123 let mut caps: Vec<u64> = Vec::new();
124 for cpu in 0.. {
125 let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpu_capacity");
126 match std::fs::read_to_string(&path) {
127 Ok(v) => caps.push(v.trim().parse().ok()?),
128 Err(_) => break,
129 }
130 }
131 Self::cores_from_capacities(&caps)
132 }
133
134 #[cfg_attr(not(all(target_arch = "aarch64", any(target_os = "linux", target_os = "android"))), allow(dead_code))]
144 fn cores_from_capacities(caps: &[u64]) -> Option<usize> {
145 let max = *caps.iter().max()?;
146 let min = *caps.iter().min()?;
147 if caps.len() < 2 || max == min {
148 return None;
149 }
150 Some(caps.iter().filter(|&&c| c * 8 >= max * 5).count())
151 }
152
153 #[cfg(target_os = "macos")]
154 fn big_cores() -> Option<usize> {
155 unsafe extern "C" {
156 fn sysctlbyname(
157 name: *const std::ffi::c_char,
158 oldp: *mut std::ffi::c_void,
159 oldlenp: *mut usize,
160 newp: *mut std::ffi::c_void,
161 newlen: usize,
162 ) -> std::ffi::c_int;
163 }
164 unsafe {
165 let name = std::ffi::CString::new("hw.perflevel0.physicalcpu").ok()?;
166 let mut count: i32 = 0;
167 let mut size = std::mem::size_of::<i32>();
168 let ret = sysctlbyname(
169 name.as_ptr(),
170 &mut count as *mut i32 as *mut std::ffi::c_void,
171 &mut size,
172 std::ptr::null_mut(),
173 0,
174 );
175 if ret == 0 && count > 0 {
176 Some(count as usize)
177 } else {
178 None
179 }
180 }
181 }
182
183 #[cfg(not(any(
184 all(target_arch = "aarch64", any(target_os = "linux", target_os = "android")),
185 target_os = "macos"
186 )))]
187 fn big_cores() -> Option<usize> {
188 None
189 }
190
191 pub fn from_env() -> Option<Arc<Self>> {
194 let n = match std::env::var("CMF_THREADS") {
195 Ok(v) => v.parse::<usize>().unwrap_or(0),
196 Err(_) => match Self::big_cores() {
197 Some(big) => big,
198 None => {
199 let avail = std::thread::available_parallelism()
200 .map(|n| n.get())
201 .unwrap_or(1);
202 avail.saturating_sub(1).min(8)
203 }
204 },
205 };
206 if n <= 1 {
207 None
208 } else {
209 Some(Arc::new(Self::new(n)))
210 }
211 }
212
213 pub fn n_workers(&self) -> usize {
215 self.threads.len()
216 }
217
218 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
229 let grain = (rows / ((self.threads.len() + 1) * 8)).max(32);
232 let next = AtomicUsize::new(0);
233 self.run(&|_w, _n| loop {
234 let start = next.fetch_add(grain, Ordering::Relaxed);
235 if start >= rows {
236 break;
237 }
238 f(start, (start + grain).min(rows));
239 });
240 }
241
242 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
250 let total: usize = parts.iter().map(|p| p.0).sum();
251 if total == 0 {
252 return;
253 }
254 let grain = (total / ((self.threads.len() + 1) * 8)).max(32);
255 let next = AtomicUsize::new(0);
256 self.run(&|_w, _n| loop {
257 let s = next.fetch_add(grain, Ordering::Relaxed);
258 if s >= total {
259 break;
260 }
261 let e = (s + grain).min(total);
262 let mut base = 0usize;
263 for &(rows, f) in parts {
264 let a = s.max(base);
265 let b = e.min(base + rows);
266 if a < b {
267 f(a - base, b - base);
268 }
269 base += rows;
270 if base >= e {
271 break;
272 }
273 }
274 });
275 }
276
277 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
281 DISPATCHES.fetch_add(1, Ordering::Relaxed);
282 let nw = self.threads.len();
283 let n = nw + 1; let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
287 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
288 unsafe { std::mem::transmute(ptr) };
289 unsafe { *self.inner.slot.get() = Some((TaskPtr(ptr), n)) };
292 self.inner.remaining.store(nw, Ordering::Relaxed);
293 self.inner.epoch.fetch_add(1, Ordering::SeqCst);
294 for (i, t) in self.threads.iter().enumerate() {
295 if self.inner.parked[i].load(Ordering::SeqCst) {
296 t.unpark();
297 }
298 }
299
300 f(nw, n);
303
304 let mut spins = 0usize;
306 while self.inner.remaining.load(Ordering::Acquire) != 0 {
307 spins += 1;
308 if spins < 10_000 {
309 std::hint::spin_loop();
310 } else {
311 std::thread::yield_now();
312 }
313 }
314 }
315}
316
317impl Drop for Pool {
318 fn drop(&mut self) {
319 self.inner.shutdown.store(true, Ordering::SeqCst);
320 for t in &self.threads {
321 t.unpark();
322 }
323 for h in self.joins.drain(..) {
324 let _ = h.join();
325 }
326 }
327}
328
329#[cfg(target_os = "android")]
330fn pin_thread_to_big_cores() {
331 use std::mem;
332 let mut caps: Vec<u64> = Vec::new();
333 for cpu in 0.. {
334 let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpu_capacity");
335 match std::fs::read_to_string(&path) {
336 Ok(v) => if let Ok(cap) = v.trim().parse() {
337 caps.push(cap);
338 } else {
339 break;
340 },
341 Err(_) => break,
342 }
343 }
344 let max = caps.iter().copied().max().unwrap_or(0);
345 let min = caps.iter().copied().min().unwrap_or(0);
346
347 if caps.len() < 2 || max == min {
349 return;
350 }
351
352 unsafe {
353 let mut set: libc::cpu_set_t = mem::zeroed();
354 for (i, &c) in caps.iter().enumerate() {
355 if c * 8 >= max * 5 {
356 libc::CPU_SET(i, &mut set);
357 }
358 }
359 libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &set);
360 }
361}
362
363fn worker_loop(inner: &Inner, idx: usize) {
364 #[cfg(target_os = "android")]
365 pin_thread_to_big_cores();
366
367 let mut seen = 0usize;
372 loop {
373 let mut spins = 0usize;
376 loop {
377 let e = inner.epoch.load(Ordering::Acquire);
378 if e != seen {
379 seen = e;
380 break;
381 }
382 if inner.shutdown.load(Ordering::Relaxed) {
383 return;
384 }
385 if spins < inner.spin_budget {
386 spins += 1;
387 std::hint::spin_loop();
388 } else {
389 inner.parked[idx].store(true, Ordering::SeqCst);
390 if inner.epoch.load(Ordering::SeqCst) == seen
395 && !inner.shutdown.load(Ordering::Relaxed)
396 {
397 std::thread::park();
398 }
399 inner.parked[idx].store(false, Ordering::SeqCst);
400 }
401 }
402 let (task, n) = unsafe { (*inner.slot.get()).expect("job published with epoch") };
406 let f = unsafe { &*task.0 };
407 f(idx, n);
408 inner.remaining.fetch_sub(1, Ordering::AcqRel);
409 }
410}
411
412pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
415 let in_dim = x.len();
416 let out_dim = out.len();
417 debug_assert!(w.len() >= out_dim * in_dim);
418
419 let row_dot = |o: usize| -> f32 {
420 let row = &w[o * in_dim..(o + 1) * in_dim];
421 let mut sum = 0.0f32;
422 for j in 0..in_dim {
423 sum += row[j] * x[j];
424 }
425 sum
426 };
427
428 match pool {
429 Some(pool) if out_dim >= 256 => {
430 let out_addr = SendMut(out.as_mut_ptr());
431 let run_range = move |start: usize, end: usize| {
432 for o in start..end {
433 unsafe { *out_addr.at(o) = row_dot(o) };
434 }
435 };
436 pool.run_rows(out_dim, &run_range);
437 }
438 _ => {
439 for (o, dst) in out.iter_mut().enumerate() {
440 *dst = row_dot(o);
441 }
442 }
443 }
444}
445
446pub fn matvec_rows2(
452 pool: Option<&Pool>,
453 w: &[f32],
454 x1: &[f32],
455 x2: &[f32],
456 out1: &mut [f32],
457 out2: &mut [f32],
458) {
459 let in_dim = x1.len();
460 debug_assert_eq!(x2.len(), in_dim);
461 let out_dim = out1.len();
462 debug_assert_eq!(out2.len(), out_dim);
463 debug_assert!(w.len() >= out_dim * in_dim);
464
465 let row_dots = |o: usize| -> (f32, f32) {
466 let row = &w[o * in_dim..(o + 1) * in_dim];
467 let (mut s1, mut s2) = (0.0f32, 0.0f32);
468 for j in 0..in_dim {
469 s1 += row[j] * x1[j];
470 s2 += row[j] * x2[j];
471 }
472 (s1, s2)
473 };
474
475 match pool {
476 Some(pool) if out_dim >= 256 => {
477 let o1 = SendMut(out1.as_mut_ptr());
478 let o2 = SendMut(out2.as_mut_ptr());
479 let run_range = move |start: usize, end: usize| {
480 for o in start..end {
481 let (s1, s2) = row_dots(o);
482 unsafe {
483 *o1.at(o) = s1;
484 *o2.at(o) = s2;
485 }
486 }
487 };
488 pool.run_rows(out_dim, &run_range);
489 }
490 _ => {
491 for o in 0..out_dim {
492 let (s1, s2) = row_dots(o);
493 out1[o] = s1;
494 out2[o] = s2;
495 }
496 }
497 }
498}
499
500#[derive(Clone, Copy)]
501struct SendMut(*mut f32);
502unsafe impl Send for SendMut {}
503unsafe impl Sync for SendMut {}
504
505impl SendMut {
506 #[inline]
509 fn at(self, i: usize) -> *mut f32 {
510 unsafe { self.0.add(i) }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 #[test]
517 fn capacity_split_clock_bins_vs_microarch() {
518 type P = super::Pool;
519 assert_eq!(P::cores_from_capacities(&[1024, 1024, 1024, 1024, 768, 768, 768, 768]), Some(8));
521 assert_eq!(P::cores_from_capacities(&[1024, 1024, 1024, 1024, 350, 350, 350, 350]), Some(4));
523 assert_eq!(
525 P::cores_from_capacities(&[1024, 800, 800, 800, 800, 300, 300, 300]),
526 Some(5)
527 );
528 assert_eq!(P::cores_from_capacities(&[1024; 8]), None);
530 assert_eq!(P::cores_from_capacities(&[]), None);
531 }
532
533 use super::*;
534
535 #[test]
536 fn parallel_matvec_equals_serial_bitexact() {
537 let (out_dim, in_dim) = (512, 64);
538 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.013).sin()).collect();
539 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
540
541 let mut serial = vec![0.0f32; out_dim];
542 matvec_rows(None, &w, &x, &mut serial);
543
544 let pool = Pool::new(4);
545 let mut parallel = vec![0.0f32; out_dim];
546 matvec_rows(Some(&pool), &w, &x, &mut parallel);
547
548 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
549 }
550
551 #[test]
552 fn fused_pair_equals_two_singles_bitexact() {
553 let (out_dim, in_dim) = (300, 48);
554 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.011).sin()).collect();
555 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
556 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
557
558 let mut a1 = vec![0.0f32; out_dim];
559 let mut a2 = vec![0.0f32; out_dim];
560 matvec_rows(None, &w, &x1, &mut a1);
561 matvec_rows(None, &w, &x2, &mut a2);
562
563 for pool in [None, Some(Pool::new(3))] {
564 let mut b1 = vec![0.0f32; out_dim];
565 let mut b2 = vec![0.0f32; out_dim];
566 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
567 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
568 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
569 }
570 }
571
572 #[test]
573 fn pool_survives_many_runs() {
574 let pool = Pool::new(3);
575 let counter = AtomicUsize::new(0);
576 for _ in 0..100 {
577 pool.run(&|_, _| {
578 counter.fetch_add(1, Ordering::Relaxed);
579 });
580 }
581 assert_eq!(counter.load(Ordering::Relaxed), 400);
583 }
584
585 #[test]
586 fn pool_wakes_after_park() {
587 let pool = Pool::with_spin(2, 0);
590 let counter = AtomicUsize::new(0);
591 for _ in 0..50 {
592 pool.run(&|_, _| {
593 counter.fetch_add(1, Ordering::Relaxed);
594 });
595 std::thread::sleep(std::time::Duration::from_micros(200));
597 }
598 assert_eq!(counter.load(Ordering::Relaxed), 150);
599 }
600
601 #[test]
602 fn worker_indices_are_distinct_and_cover_range() {
603 let pool = Pool::new(3);
604 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
605 for _ in 0..20 {
606 pool.run(&|widx, n| {
607 assert_eq!(n, 4);
608 hits[widx].fetch_add(1, Ordering::Relaxed);
609 });
610 }
611 for (i, h) in hits.iter().enumerate() {
612 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
613 }
614 }
615}