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 let max = *caps.iter().max()?;
132 let min = *caps.iter().min()?;
133 if caps.len() < 2 || max == min {
134 return None;
135 }
136 Some(caps.iter().filter(|&&c| c == max).count())
137 }
138
139 #[cfg(not(all(target_arch = "aarch64", any(target_os = "linux", target_os = "android"))))]
140 fn big_cores() -> Option<usize> {
141 None
142 }
143
144 pub fn from_env() -> Option<Arc<Self>> {
147 let n = match std::env::var("CMF_THREADS") {
148 Ok(v) => v.parse::<usize>().unwrap_or(0),
149 Err(_) => match Self::big_cores() {
150 Some(big) => big,
151 None => {
152 let avail = std::thread::available_parallelism()
153 .map(|n| n.get())
154 .unwrap_or(1);
155 avail.saturating_sub(1).min(8)
156 }
157 },
158 };
159 if n <= 1 {
160 None
161 } else {
162 Some(Arc::new(Self::new(n)))
163 }
164 }
165
166 pub fn n_workers(&self) -> usize {
168 self.threads.len()
169 }
170
171 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
182 let grain = (rows / ((self.threads.len() + 1) * 8)).max(32);
185 let next = AtomicUsize::new(0);
186 self.run(&|_w, _n| loop {
187 let start = next.fetch_add(grain, Ordering::Relaxed);
188 if start >= rows {
189 break;
190 }
191 f(start, (start + grain).min(rows));
192 });
193 }
194
195 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
203 let total: usize = parts.iter().map(|p| p.0).sum();
204 if total == 0 {
205 return;
206 }
207 let grain = (total / ((self.threads.len() + 1) * 8)).max(32);
208 let next = AtomicUsize::new(0);
209 self.run(&|_w, _n| loop {
210 let s = next.fetch_add(grain, Ordering::Relaxed);
211 if s >= total {
212 break;
213 }
214 let e = (s + grain).min(total);
215 let mut base = 0usize;
216 for &(rows, f) in parts {
217 let a = s.max(base);
218 let b = e.min(base + rows);
219 if a < b {
220 f(a - base, b - base);
221 }
222 base += rows;
223 if base >= e {
224 break;
225 }
226 }
227 });
228 }
229
230 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
234 DISPATCHES.fetch_add(1, Ordering::Relaxed);
235 let nw = self.threads.len();
236 let n = nw + 1; let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
240 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
241 unsafe { std::mem::transmute(ptr) };
242 unsafe { *self.inner.slot.get() = Some((TaskPtr(ptr), n)) };
245 self.inner.remaining.store(nw, Ordering::Relaxed);
246 self.inner.epoch.fetch_add(1, Ordering::SeqCst);
247 for (i, t) in self.threads.iter().enumerate() {
248 if self.inner.parked[i].load(Ordering::SeqCst) {
249 t.unpark();
250 }
251 }
252
253 f(nw, n);
256
257 let mut spins = 0usize;
259 while self.inner.remaining.load(Ordering::Acquire) != 0 {
260 spins += 1;
261 if spins < 10_000 {
262 std::hint::spin_loop();
263 } else {
264 std::thread::yield_now();
265 }
266 }
267 }
268}
269
270impl Drop for Pool {
271 fn drop(&mut self) {
272 self.inner.shutdown.store(true, Ordering::SeqCst);
273 for t in &self.threads {
274 t.unpark();
275 }
276 for h in self.joins.drain(..) {
277 let _ = h.join();
278 }
279 }
280}
281
282fn worker_loop(inner: &Inner, idx: usize) {
283 let mut seen = 0usize;
288 loop {
289 let mut spins = 0usize;
292 loop {
293 let e = inner.epoch.load(Ordering::Acquire);
294 if e != seen {
295 seen = e;
296 break;
297 }
298 if inner.shutdown.load(Ordering::Relaxed) {
299 return;
300 }
301 if spins < inner.spin_budget {
302 spins += 1;
303 std::hint::spin_loop();
304 } else {
305 inner.parked[idx].store(true, Ordering::SeqCst);
306 if inner.epoch.load(Ordering::SeqCst) == seen
311 && !inner.shutdown.load(Ordering::Relaxed)
312 {
313 std::thread::park();
314 }
315 inner.parked[idx].store(false, Ordering::SeqCst);
316 }
317 }
318 let (task, n) = unsafe { (*inner.slot.get()).expect("job published with epoch") };
322 let f = unsafe { &*task.0 };
323 f(idx, n);
324 inner.remaining.fetch_sub(1, Ordering::AcqRel);
325 }
326}
327
328pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
331 let in_dim = x.len();
332 let out_dim = out.len();
333 debug_assert!(w.len() >= out_dim * in_dim);
334
335 let row_dot = |o: usize| -> f32 {
336 let row = &w[o * in_dim..(o + 1) * in_dim];
337 let mut sum = 0.0f32;
338 for j in 0..in_dim {
339 sum += row[j] * x[j];
340 }
341 sum
342 };
343
344 match pool {
345 Some(pool) if out_dim >= 256 => {
347 let out_addr = SendMut(out.as_mut_ptr());
348 pool.run(&move |widx, n| {
349 let chunk = out_dim.div_ceil(n);
350 let start = widx * chunk;
351 let end = (start + chunk).min(out_dim);
352 for o in start..end {
353 unsafe { *out_addr.at(o) = row_dot(o) };
355 }
356 });
357 }
358 _ => {
359 for (o, dst) in out.iter_mut().enumerate() {
360 *dst = row_dot(o);
361 }
362 }
363 }
364}
365
366pub fn matvec_rows2(
372 pool: Option<&Pool>,
373 w: &[f32],
374 x1: &[f32],
375 x2: &[f32],
376 out1: &mut [f32],
377 out2: &mut [f32],
378) {
379 let in_dim = x1.len();
380 debug_assert_eq!(x2.len(), in_dim);
381 let out_dim = out1.len();
382 debug_assert_eq!(out2.len(), out_dim);
383 debug_assert!(w.len() >= out_dim * in_dim);
384
385 let row_dots = |o: usize| -> (f32, f32) {
386 let row = &w[o * in_dim..(o + 1) * in_dim];
387 let (mut s1, mut s2) = (0.0f32, 0.0f32);
388 for j in 0..in_dim {
389 s1 += row[j] * x1[j];
390 s2 += row[j] * x2[j];
391 }
392 (s1, s2)
393 };
394
395 match pool {
396 Some(pool) if out_dim >= 256 => {
397 let o1 = SendMut(out1.as_mut_ptr());
398 let o2 = SendMut(out2.as_mut_ptr());
399 pool.run(&move |widx, n| {
400 let chunk = out_dim.div_ceil(n);
401 let start = widx * chunk;
402 let end = (start + chunk).min(out_dim);
403 for o in start..end {
404 let (s1, s2) = row_dots(o);
405 unsafe {
407 *o1.at(o) = s1;
408 *o2.at(o) = s2;
409 }
410 }
411 });
412 }
413 _ => {
414 for o in 0..out_dim {
415 let (s1, s2) = row_dots(o);
416 out1[o] = s1;
417 out2[o] = s2;
418 }
419 }
420 }
421}
422
423#[derive(Clone, Copy)]
424struct SendMut(*mut f32);
425unsafe impl Send for SendMut {}
426unsafe impl Sync for SendMut {}
427
428impl SendMut {
429 #[inline]
432 fn at(self, i: usize) -> *mut f32 {
433 unsafe { self.0.add(i) }
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn parallel_matvec_equals_serial_bitexact() {
443 let (out_dim, in_dim) = (512, 64);
444 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.013).sin()).collect();
445 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
446
447 let mut serial = vec![0.0f32; out_dim];
448 matvec_rows(None, &w, &x, &mut serial);
449
450 let pool = Pool::new(4);
451 let mut parallel = vec![0.0f32; out_dim];
452 matvec_rows(Some(&pool), &w, &x, &mut parallel);
453
454 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
455 }
456
457 #[test]
458 fn fused_pair_equals_two_singles_bitexact() {
459 let (out_dim, in_dim) = (300, 48);
460 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.011).sin()).collect();
461 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
462 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
463
464 let mut a1 = vec![0.0f32; out_dim];
465 let mut a2 = vec![0.0f32; out_dim];
466 matvec_rows(None, &w, &x1, &mut a1);
467 matvec_rows(None, &w, &x2, &mut a2);
468
469 for pool in [None, Some(Pool::new(3))] {
470 let mut b1 = vec![0.0f32; out_dim];
471 let mut b2 = vec![0.0f32; out_dim];
472 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
473 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
474 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
475 }
476 }
477
478 #[test]
479 fn pool_survives_many_runs() {
480 let pool = Pool::new(3);
481 let counter = AtomicUsize::new(0);
482 for _ in 0..100 {
483 pool.run(&|_, _| {
484 counter.fetch_add(1, Ordering::Relaxed);
485 });
486 }
487 assert_eq!(counter.load(Ordering::Relaxed), 400);
489 }
490
491 #[test]
492 fn pool_wakes_after_park() {
493 let pool = Pool::with_spin(2, 0);
496 let counter = AtomicUsize::new(0);
497 for _ in 0..50 {
498 pool.run(&|_, _| {
499 counter.fetch_add(1, Ordering::Relaxed);
500 });
501 std::thread::sleep(std::time::Duration::from_micros(200));
503 }
504 assert_eq!(counter.load(Ordering::Relaxed), 150);
505 }
506
507 #[test]
508 fn worker_indices_are_distinct_and_cover_range() {
509 let pool = Pool::new(3);
510 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
511 for _ in 0..20 {
512 pool.run(&|widx, n| {
513 assert_eq!(n, 4);
514 hits[widx].fetch_add(1, Ordering::Relaxed);
515 });
516 }
517 for (i, h) in hits.iter().enumerate() {
518 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
519 }
520 }
521}