1use std::cell::UnsafeCell;
31use std::sync::Arc;
32use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
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 {
114 inner,
115 threads,
116 joins,
117 }
118 }
119
120 #[cfg(all(
126 target_arch = "aarch64",
127 any(target_os = "linux", target_os = "android")
128 ))]
129 fn big_cores() -> Option<usize> {
130 let mut caps: Vec<u64> = Vec::new();
131 for cpu in 0.. {
132 let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpu_capacity");
133 match std::fs::read_to_string(&path) {
134 Ok(v) => caps.push(v.trim().parse().ok()?),
135 Err(_) => break,
136 }
137 }
138 Self::cores_from_capacities(&caps)
139 }
140
141 #[cfg_attr(
151 not(all(
152 target_arch = "aarch64",
153 any(target_os = "linux", target_os = "android")
154 )),
155 allow(dead_code)
156 )]
157 fn cores_from_capacities(caps: &[u64]) -> Option<usize> {
158 let max = *caps.iter().max()?;
159 let min = *caps.iter().min()?;
160 if caps.len() < 2 || max == min {
161 return None;
162 }
163 Some(caps.iter().filter(|&&c| c * 8 >= max * 5).count())
164 }
165
166 #[cfg(target_os = "macos")]
167 fn big_cores() -> Option<usize> {
168 unsafe extern "C" {
169 fn sysctlbyname(
170 name: *const std::ffi::c_char,
171 oldp: *mut std::ffi::c_void,
172 oldlenp: *mut usize,
173 newp: *mut std::ffi::c_void,
174 newlen: usize,
175 ) -> std::ffi::c_int;
176 }
177 unsafe {
178 let name = std::ffi::CString::new("hw.perflevel0.physicalcpu").ok()?;
179 let mut count: i32 = 0;
180 let mut size = std::mem::size_of::<i32>();
181 let ret = sysctlbyname(
182 name.as_ptr(),
183 &mut count as *mut i32 as *mut std::ffi::c_void,
184 &mut size,
185 std::ptr::null_mut(),
186 0,
187 );
188 if ret == 0 && count > 0 {
189 Some(count as usize)
190 } else {
191 None
192 }
193 }
194 }
195
196 #[cfg(not(any(
197 all(
198 target_arch = "aarch64",
199 any(target_os = "linux", target_os = "android")
200 ),
201 target_os = "macos"
202 )))]
203 fn big_cores() -> Option<usize> {
204 None
205 }
206
207 pub fn from_env() -> Option<Arc<Self>> {
210 let n = match std::env::var("CMF_THREADS") {
211 Ok(v) => v.parse::<usize>().unwrap_or(0),
212 Err(_) => match Self::big_cores() {
213 Some(big) => big,
214 None => {
215 let avail = std::thread::available_parallelism()
216 .map(|n| n.get())
217 .unwrap_or(1);
218 avail.saturating_sub(1).min(8)
219 }
220 },
221 };
222 if n <= 1 {
223 None
224 } else {
225 Some(Arc::new(Self::new(n)))
226 }
227 }
228
229 pub fn n_workers(&self) -> usize {
231 self.threads.len()
232 }
233
234 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
245 let grain = (rows / ((self.threads.len() + 1) * 8)).max(32);
248 let next = AtomicUsize::new(0);
249 self.run(&|_w, _n| loop {
250 let start = next.fetch_add(grain, Ordering::Relaxed);
251 if start >= rows {
252 break;
253 }
254 f(start, (start + grain).min(rows));
255 });
256 }
257
258 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
266 let total: usize = parts.iter().map(|p| p.0).sum();
267 if total == 0 {
268 return;
269 }
270 let grain = (total / ((self.threads.len() + 1) * 8)).max(32);
271 let next = AtomicUsize::new(0);
272 self.run(&|_w, _n| loop {
273 let s = next.fetch_add(grain, Ordering::Relaxed);
274 if s >= total {
275 break;
276 }
277 let e = (s + grain).min(total);
278 let mut base = 0usize;
279 for &(rows, f) in parts {
280 let a = s.max(base);
281 let b = e.min(base + rows);
282 if a < b {
283 f(a - base, b - base);
284 }
285 base += rows;
286 if base >= e {
287 break;
288 }
289 }
290 });
291 }
292
293 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
297 DISPATCHES.fetch_add(1, Ordering::Relaxed);
298 let nw = self.threads.len();
299 let n = nw + 1; let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
303 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
304 unsafe { std::mem::transmute(ptr) };
305 unsafe { *self.inner.slot.get() = Some((TaskPtr(ptr), n)) };
308 self.inner.remaining.store(nw, Ordering::Relaxed);
309 self.inner.epoch.fetch_add(1, Ordering::SeqCst);
310 for (i, t) in self.threads.iter().enumerate() {
311 if self.inner.parked[i].load(Ordering::SeqCst) {
312 t.unpark();
313 }
314 }
315
316 f(nw, n);
319
320 let mut spins = 0usize;
322 while self.inner.remaining.load(Ordering::Acquire) != 0 {
323 spins += 1;
324 if spins < 10_000 {
325 std::hint::spin_loop();
326 } else {
327 std::thread::yield_now();
328 }
329 }
330 }
331}
332
333impl Drop for Pool {
334 fn drop(&mut self) {
335 self.inner.shutdown.store(true, Ordering::SeqCst);
336 for t in &self.threads {
337 t.unpark();
338 }
339 for h in self.joins.drain(..) {
340 let _ = h.join();
341 }
342 }
343}
344
345#[cfg(target_os = "android")]
346fn pin_thread_to_big_cores() {
347 use std::mem;
348 let mut caps: Vec<u64> = Vec::new();
349 for cpu in 0.. {
350 let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpu_capacity");
351 match std::fs::read_to_string(&path) {
352 Ok(v) => {
353 if let Ok(cap) = v.trim().parse() {
354 caps.push(cap);
355 } else {
356 break;
357 }
358 }
359 Err(_) => break,
360 }
361 }
362 let max = caps.iter().copied().max().unwrap_or(0);
363 let min = caps.iter().copied().min().unwrap_or(0);
364
365 if caps.len() < 2 || max == min {
367 return;
368 }
369
370 unsafe {
371 let mut set: libc::cpu_set_t = mem::zeroed();
372 for (i, &c) in caps.iter().enumerate() {
373 if c * 8 >= max * 5 {
374 libc::CPU_SET(i, &mut set);
375 }
376 }
377 libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &set);
378 }
379}
380
381fn worker_loop(inner: &Inner, idx: usize) {
382 #[cfg(target_os = "android")]
383 pin_thread_to_big_cores();
384
385 let mut seen = 0usize;
390 loop {
391 let mut spins = 0usize;
394 loop {
395 let e = inner.epoch.load(Ordering::Acquire);
396 if e != seen {
397 seen = e;
398 break;
399 }
400 if inner.shutdown.load(Ordering::Relaxed) {
401 return;
402 }
403 if spins < inner.spin_budget {
404 spins += 1;
405 std::hint::spin_loop();
406 } else {
407 inner.parked[idx].store(true, Ordering::SeqCst);
408 if inner.epoch.load(Ordering::SeqCst) == seen
413 && !inner.shutdown.load(Ordering::Relaxed)
414 {
415 std::thread::park();
416 }
417 inner.parked[idx].store(false, Ordering::SeqCst);
418 }
419 }
420 let (task, n) = unsafe { (*inner.slot.get()).expect("job published with epoch") };
424 let f = unsafe { &*task.0 };
425 f(idx, n);
426 inner.remaining.fetch_sub(1, Ordering::AcqRel);
427 }
428}
429
430pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
433 let in_dim = x.len();
434 let out_dim = out.len();
435 debug_assert!(w.len() >= out_dim * in_dim);
436
437 let row_dot = |o: usize| -> f32 {
438 let row = &w[o * in_dim..(o + 1) * in_dim];
439 let mut sum = 0.0f32;
440 for j in 0..in_dim {
441 sum += row[j] * x[j];
442 }
443 sum
444 };
445
446 match pool {
447 Some(pool) if out_dim >= 256 => {
448 let out_addr = SendMut(out.as_mut_ptr());
449 let run_range = move |start: usize, end: usize| {
450 for o in start..end {
451 unsafe { *out_addr.at(o) = row_dot(o) };
452 }
453 };
454 pool.run_rows(out_dim, &run_range);
455 }
456 _ => {
457 for (o, dst) in out.iter_mut().enumerate() {
458 *dst = row_dot(o);
459 }
460 }
461 }
462}
463
464pub fn matvec_rows2(
470 pool: Option<&Pool>,
471 w: &[f32],
472 x1: &[f32],
473 x2: &[f32],
474 out1: &mut [f32],
475 out2: &mut [f32],
476) {
477 let in_dim = x1.len();
478 debug_assert_eq!(x2.len(), in_dim);
479 let out_dim = out1.len();
480 debug_assert_eq!(out2.len(), out_dim);
481 debug_assert!(w.len() >= out_dim * in_dim);
482
483 let row_dots = |o: usize| -> (f32, f32) {
484 let row = &w[o * in_dim..(o + 1) * in_dim];
485 let (mut s1, mut s2) = (0.0f32, 0.0f32);
486 for j in 0..in_dim {
487 s1 += row[j] * x1[j];
488 s2 += row[j] * x2[j];
489 }
490 (s1, s2)
491 };
492
493 match pool {
494 Some(pool) if out_dim >= 256 => {
495 let o1 = SendMut(out1.as_mut_ptr());
496 let o2 = SendMut(out2.as_mut_ptr());
497 let run_range = move |start: usize, end: usize| {
498 for o in start..end {
499 let (s1, s2) = row_dots(o);
500 unsafe {
501 *o1.at(o) = s1;
502 *o2.at(o) = s2;
503 }
504 }
505 };
506 pool.run_rows(out_dim, &run_range);
507 }
508 _ => {
509 for o in 0..out_dim {
510 let (s1, s2) = row_dots(o);
511 out1[o] = s1;
512 out2[o] = s2;
513 }
514 }
515 }
516}
517
518#[derive(Clone, Copy)]
519struct SendMut(*mut f32);
520unsafe impl Send for SendMut {}
521unsafe impl Sync for SendMut {}
522
523impl SendMut {
524 #[inline]
527 fn at(self, i: usize) -> *mut f32 {
528 unsafe { self.0.add(i) }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 #[test]
535 fn capacity_split_clock_bins_vs_microarch() {
536 type P = super::Pool;
537 assert_eq!(
539 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 768, 768, 768, 768]),
540 Some(8)
541 );
542 assert_eq!(
544 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 350, 350, 350, 350]),
545 Some(4)
546 );
547 assert_eq!(
549 P::cores_from_capacities(&[1024, 800, 800, 800, 800, 300, 300, 300]),
550 Some(5)
551 );
552 assert_eq!(P::cores_from_capacities(&[1024; 8]), None);
554 assert_eq!(P::cores_from_capacities(&[]), None);
555 }
556
557 use super::*;
558
559 #[test]
560 fn parallel_matvec_equals_serial_bitexact() {
561 let (out_dim, in_dim) = (512, 64);
562 let w: Vec<f32> = (0..out_dim * in_dim)
563 .map(|i| (i as f32 * 0.013).sin())
564 .collect();
565 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
566
567 let mut serial = vec![0.0f32; out_dim];
568 matvec_rows(None, &w, &x, &mut serial);
569
570 let pool = Pool::new(4);
571 let mut parallel = vec![0.0f32; out_dim];
572 matvec_rows(Some(&pool), &w, &x, &mut parallel);
573
574 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
575 }
576
577 #[test]
578 fn fused_pair_equals_two_singles_bitexact() {
579 let (out_dim, in_dim) = (300, 48);
580 let w: Vec<f32> = (0..out_dim * in_dim)
581 .map(|i| (i as f32 * 0.011).sin())
582 .collect();
583 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
584 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
585
586 let mut a1 = vec![0.0f32; out_dim];
587 let mut a2 = vec![0.0f32; out_dim];
588 matvec_rows(None, &w, &x1, &mut a1);
589 matvec_rows(None, &w, &x2, &mut a2);
590
591 for pool in [None, Some(Pool::new(3))] {
592 let mut b1 = vec![0.0f32; out_dim];
593 let mut b2 = vec![0.0f32; out_dim];
594 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
595 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
596 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
597 }
598 }
599
600 #[test]
601 fn pool_survives_many_runs() {
602 let pool = Pool::new(3);
603 let counter = AtomicUsize::new(0);
604 for _ in 0..100 {
605 pool.run(&|_, _| {
606 counter.fetch_add(1, Ordering::Relaxed);
607 });
608 }
609 assert_eq!(counter.load(Ordering::Relaxed), 400);
611 }
612
613 #[test]
614 fn pool_wakes_after_park() {
615 let pool = Pool::with_spin(2, 0);
618 let counter = AtomicUsize::new(0);
619 for _ in 0..50 {
620 pool.run(&|_, _| {
621 counter.fetch_add(1, Ordering::Relaxed);
622 });
623 std::thread::sleep(std::time::Duration::from_micros(200));
625 }
626 assert_eq!(counter.load(Ordering::Relaxed), 150);
627 }
628
629 #[test]
630 fn worker_indices_are_distinct_and_cover_range() {
631 let pool = Pool::new(3);
632 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
633 for _ in 0..20 {
634 pool.run(&|widx, n| {
635 assert_eq!(n, 4);
636 hits[widx].fetch_add(1, Ordering::Relaxed);
637 });
638 }
639 for (i, h) in hits.iter().enumerate() {
640 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
641 }
642 }
643}