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 pub fn from_env() -> Option<Arc<Self>> {
118 let n = match std::env::var("CMF_THREADS") {
119 Ok(v) => v.parse::<usize>().unwrap_or(0),
120 Err(_) => {
121 let avail = std::thread::available_parallelism()
122 .map(|n| n.get())
123 .unwrap_or(1);
124 avail.saturating_sub(1).min(8)
125 }
126 };
127 if n <= 1 {
128 None
129 } else {
130 Some(Arc::new(Self::new(n)))
131 }
132 }
133
134 pub fn n_workers(&self) -> usize {
136 self.threads.len()
137 }
138
139 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
150 let grain = (rows / ((self.threads.len() + 1) * 8)).max(32);
153 let next = AtomicUsize::new(0);
154 self.run(&|_w, _n| loop {
155 let start = next.fetch_add(grain, Ordering::Relaxed);
156 if start >= rows {
157 break;
158 }
159 f(start, (start + grain).min(rows));
160 });
161 }
162
163 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
171 let total: usize = parts.iter().map(|p| p.0).sum();
172 if total == 0 {
173 return;
174 }
175 let grain = (total / ((self.threads.len() + 1) * 8)).max(32);
176 let next = AtomicUsize::new(0);
177 self.run(&|_w, _n| loop {
178 let s = next.fetch_add(grain, Ordering::Relaxed);
179 if s >= total {
180 break;
181 }
182 let e = (s + grain).min(total);
183 let mut base = 0usize;
184 for &(rows, f) in parts {
185 let a = s.max(base);
186 let b = e.min(base + rows);
187 if a < b {
188 f(a - base, b - base);
189 }
190 base += rows;
191 if base >= e {
192 break;
193 }
194 }
195 });
196 }
197
198 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
202 DISPATCHES.fetch_add(1, Ordering::Relaxed);
203 let nw = self.threads.len();
204 let n = nw + 1; let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
208 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
209 unsafe { std::mem::transmute(ptr) };
210 unsafe { *self.inner.slot.get() = Some((TaskPtr(ptr), n)) };
213 self.inner.remaining.store(nw, Ordering::Relaxed);
214 self.inner.epoch.fetch_add(1, Ordering::SeqCst);
215 for (i, t) in self.threads.iter().enumerate() {
216 if self.inner.parked[i].load(Ordering::SeqCst) {
217 t.unpark();
218 }
219 }
220
221 f(nw, n);
224
225 let mut spins = 0usize;
227 while self.inner.remaining.load(Ordering::Acquire) != 0 {
228 spins += 1;
229 if spins < 10_000 {
230 std::hint::spin_loop();
231 } else {
232 std::thread::yield_now();
233 }
234 }
235 }
236}
237
238impl Drop for Pool {
239 fn drop(&mut self) {
240 self.inner.shutdown.store(true, Ordering::SeqCst);
241 for t in &self.threads {
242 t.unpark();
243 }
244 for h in self.joins.drain(..) {
245 let _ = h.join();
246 }
247 }
248}
249
250fn worker_loop(inner: &Inner, idx: usize) {
251 let mut seen = 0usize;
256 loop {
257 let mut spins = 0usize;
260 loop {
261 let e = inner.epoch.load(Ordering::Acquire);
262 if e != seen {
263 seen = e;
264 break;
265 }
266 if inner.shutdown.load(Ordering::Relaxed) {
267 return;
268 }
269 if spins < inner.spin_budget {
270 spins += 1;
271 std::hint::spin_loop();
272 } else {
273 inner.parked[idx].store(true, Ordering::SeqCst);
274 if inner.epoch.load(Ordering::SeqCst) == seen
279 && !inner.shutdown.load(Ordering::Relaxed)
280 {
281 std::thread::park();
282 }
283 inner.parked[idx].store(false, Ordering::SeqCst);
284 }
285 }
286 let (task, n) = unsafe { (*inner.slot.get()).expect("job published with epoch") };
290 let f = unsafe { &*task.0 };
291 f(idx, n);
292 inner.remaining.fetch_sub(1, Ordering::AcqRel);
293 }
294}
295
296pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
299 let in_dim = x.len();
300 let out_dim = out.len();
301 debug_assert!(w.len() >= out_dim * in_dim);
302
303 let row_dot = |o: usize| -> f32 {
304 let row = &w[o * in_dim..(o + 1) * in_dim];
305 let mut sum = 0.0f32;
306 for j in 0..in_dim {
307 sum += row[j] * x[j];
308 }
309 sum
310 };
311
312 match pool {
313 Some(pool) if out_dim >= 256 => {
315 let out_addr = SendMut(out.as_mut_ptr());
316 pool.run(&move |widx, n| {
317 let chunk = out_dim.div_ceil(n);
318 let start = widx * chunk;
319 let end = (start + chunk).min(out_dim);
320 for o in start..end {
321 unsafe { *out_addr.at(o) = row_dot(o) };
323 }
324 });
325 }
326 _ => {
327 for (o, dst) in out.iter_mut().enumerate() {
328 *dst = row_dot(o);
329 }
330 }
331 }
332}
333
334pub fn matvec_rows2(
340 pool: Option<&Pool>,
341 w: &[f32],
342 x1: &[f32],
343 x2: &[f32],
344 out1: &mut [f32],
345 out2: &mut [f32],
346) {
347 let in_dim = x1.len();
348 debug_assert_eq!(x2.len(), in_dim);
349 let out_dim = out1.len();
350 debug_assert_eq!(out2.len(), out_dim);
351 debug_assert!(w.len() >= out_dim * in_dim);
352
353 let row_dots = |o: usize| -> (f32, f32) {
354 let row = &w[o * in_dim..(o + 1) * in_dim];
355 let (mut s1, mut s2) = (0.0f32, 0.0f32);
356 for j in 0..in_dim {
357 s1 += row[j] * x1[j];
358 s2 += row[j] * x2[j];
359 }
360 (s1, s2)
361 };
362
363 match pool {
364 Some(pool) if out_dim >= 256 => {
365 let o1 = SendMut(out1.as_mut_ptr());
366 let o2 = SendMut(out2.as_mut_ptr());
367 pool.run(&move |widx, n| {
368 let chunk = out_dim.div_ceil(n);
369 let start = widx * chunk;
370 let end = (start + chunk).min(out_dim);
371 for o in start..end {
372 let (s1, s2) = row_dots(o);
373 unsafe {
375 *o1.at(o) = s1;
376 *o2.at(o) = s2;
377 }
378 }
379 });
380 }
381 _ => {
382 for o in 0..out_dim {
383 let (s1, s2) = row_dots(o);
384 out1[o] = s1;
385 out2[o] = s2;
386 }
387 }
388 }
389}
390
391#[derive(Clone, Copy)]
392struct SendMut(*mut f32);
393unsafe impl Send for SendMut {}
394unsafe impl Sync for SendMut {}
395
396impl SendMut {
397 #[inline]
400 fn at(self, i: usize) -> *mut f32 {
401 unsafe { self.0.add(i) }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn parallel_matvec_equals_serial_bitexact() {
411 let (out_dim, in_dim) = (512, 64);
412 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.013).sin()).collect();
413 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
414
415 let mut serial = vec![0.0f32; out_dim];
416 matvec_rows(None, &w, &x, &mut serial);
417
418 let pool = Pool::new(4);
419 let mut parallel = vec![0.0f32; out_dim];
420 matvec_rows(Some(&pool), &w, &x, &mut parallel);
421
422 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
423 }
424
425 #[test]
426 fn fused_pair_equals_two_singles_bitexact() {
427 let (out_dim, in_dim) = (300, 48);
428 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.011).sin()).collect();
429 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
430 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
431
432 let mut a1 = vec![0.0f32; out_dim];
433 let mut a2 = vec![0.0f32; out_dim];
434 matvec_rows(None, &w, &x1, &mut a1);
435 matvec_rows(None, &w, &x2, &mut a2);
436
437 for pool in [None, Some(Pool::new(3))] {
438 let mut b1 = vec![0.0f32; out_dim];
439 let mut b2 = vec![0.0f32; out_dim];
440 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
441 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
442 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
443 }
444 }
445
446 #[test]
447 fn pool_survives_many_runs() {
448 let pool = Pool::new(3);
449 let counter = AtomicUsize::new(0);
450 for _ in 0..100 {
451 pool.run(&|_, _| {
452 counter.fetch_add(1, Ordering::Relaxed);
453 });
454 }
455 assert_eq!(counter.load(Ordering::Relaxed), 400);
457 }
458
459 #[test]
460 fn pool_wakes_after_park() {
461 let pool = Pool::with_spin(2, 0);
464 let counter = AtomicUsize::new(0);
465 for _ in 0..50 {
466 pool.run(&|_, _| {
467 counter.fetch_add(1, Ordering::Relaxed);
468 });
469 std::thread::sleep(std::time::Duration::from_micros(200));
471 }
472 assert_eq!(counter.load(Ordering::Relaxed), 150);
473 }
474
475 #[test]
476 fn worker_indices_are_distinct_and_cover_range() {
477 let pool = Pool::new(3);
478 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
479 for _ in 0..20 {
480 pool.run(&|widx, n| {
481 assert_eq!(n, 4);
482 hits[widx].fetch_add(1, Ordering::Relaxed);
483 });
484 }
485 for (i, h) in hits.iter().enumerate() {
486 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
487 }
488 }
489}