1use std::cell::UnsafeCell;
26use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
27use std::sync::Arc;
28
29#[derive(Clone, Copy)]
33struct TaskPtr(*const (dyn Fn(usize, usize) + Sync));
34unsafe impl Send for TaskPtr {}
35
36struct Inner {
37 epoch: AtomicUsize,
39 remaining: AtomicUsize,
41 slot: UnsafeCell<Option<(TaskPtr, usize)>>,
45 shutdown: AtomicBool,
46 spin_budget: usize,
48 parked: Box<[AtomicBool]>,
51}
52
53unsafe impl Sync for Inner {}
57
58static DISPATCHES: AtomicUsize = AtomicUsize::new(0);
61
62pub fn dispatch_count() -> usize {
64 DISPATCHES.load(Ordering::Relaxed)
65}
66
67pub struct Pool {
70 inner: Arc<Inner>,
71 threads: Vec<std::thread::Thread>,
73 joins: Vec<std::thread::JoinHandle<()>>,
74}
75
76fn spin_budget_from_env() -> usize {
77 std::env::var("CMF_POOL_SPIN")
78 .ok()
79 .and_then(|v| v.parse::<usize>().ok())
80 .unwrap_or(0)
81}
82
83impl Pool {
84 pub fn new(n_workers: usize) -> Self {
85 Self::with_spin(n_workers, spin_budget_from_env())
86 }
87
88 pub fn with_spin(n_workers: usize, spin_budget: usize) -> Self {
90 let inner = Arc::new(Inner {
91 epoch: AtomicUsize::new(0),
92 remaining: AtomicUsize::new(0),
93 slot: UnsafeCell::new(None),
94 shutdown: AtomicBool::new(false),
95 spin_budget,
96 parked: (0..n_workers).map(|_| AtomicBool::new(false)).collect(),
97 });
98 let mut joins = Vec::with_capacity(n_workers);
99 for w in 0..n_workers {
100 let inner = inner.clone();
101 let h = std::thread::Builder::new()
102 .name(format!("cmf-pool-{w}"))
103 .spawn(move || worker_loop(&inner, w))
104 .expect("spawn pool worker");
105 joins.push(h);
106 }
107 let threads = joins.iter().map(|h| h.thread().clone()).collect();
108 Self { inner, threads, joins }
109 }
110
111 pub fn from_env() -> Option<Arc<Self>> {
113 let n = match std::env::var("CMF_THREADS") {
114 Ok(v) => v.parse::<usize>().unwrap_or(0),
115 Err(_) => {
116 let avail = std::thread::available_parallelism()
117 .map(|n| n.get())
118 .unwrap_or(1);
119 avail.saturating_sub(1).min(8)
120 }
121 };
122 if n <= 1 {
123 None
124 } else {
125 Some(Arc::new(Self::new(n)))
126 }
127 }
128
129 pub fn n_workers(&self) -> usize {
131 self.threads.len()
132 }
133
134 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
145 let grain = (rows / ((self.threads.len() + 1) * 8)).max(32);
148 let next = AtomicUsize::new(0);
149 self.run(&|_w, _n| loop {
150 let start = next.fetch_add(grain, Ordering::Relaxed);
151 if start >= rows {
152 break;
153 }
154 f(start, (start + grain).min(rows));
155 });
156 }
157
158 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
166 let total: usize = parts.iter().map(|p| p.0).sum();
167 if total == 0 {
168 return;
169 }
170 let grain = (total / ((self.threads.len() + 1) * 8)).max(32);
171 let next = AtomicUsize::new(0);
172 self.run(&|_w, _n| loop {
173 let s = next.fetch_add(grain, Ordering::Relaxed);
174 if s >= total {
175 break;
176 }
177 let e = (s + grain).min(total);
178 let mut base = 0usize;
179 for &(rows, f) in parts {
180 let a = s.max(base);
181 let b = e.min(base + rows);
182 if a < b {
183 f(a - base, b - base);
184 }
185 base += rows;
186 if base >= e {
187 break;
188 }
189 }
190 });
191 }
192
193 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
197 DISPATCHES.fetch_add(1, Ordering::Relaxed);
198 let nw = self.threads.len();
199 let n = nw + 1; let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
203 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
204 unsafe { std::mem::transmute(ptr) };
205 unsafe { *self.inner.slot.get() = Some((TaskPtr(ptr), n)) };
208 self.inner.remaining.store(nw, Ordering::Relaxed);
209 self.inner.epoch.fetch_add(1, Ordering::SeqCst);
210 for (i, t) in self.threads.iter().enumerate() {
211 if self.inner.parked[i].load(Ordering::SeqCst) {
212 t.unpark();
213 }
214 }
215
216 f(nw, n);
219
220 let mut spins = 0usize;
222 while self.inner.remaining.load(Ordering::Acquire) != 0 {
223 spins += 1;
224 if spins < 10_000 {
225 std::hint::spin_loop();
226 } else {
227 std::thread::yield_now();
228 }
229 }
230 }
231}
232
233impl Drop for Pool {
234 fn drop(&mut self) {
235 self.inner.shutdown.store(true, Ordering::SeqCst);
236 for t in &self.threads {
237 t.unpark();
238 }
239 for h in self.joins.drain(..) {
240 let _ = h.join();
241 }
242 }
243}
244
245fn worker_loop(inner: &Inner, idx: usize) {
246 let mut seen = 0usize;
251 loop {
252 let mut spins = 0usize;
255 loop {
256 let e = inner.epoch.load(Ordering::Acquire);
257 if e != seen {
258 seen = e;
259 break;
260 }
261 if inner.shutdown.load(Ordering::Relaxed) {
262 return;
263 }
264 if spins < inner.spin_budget {
265 spins += 1;
266 std::hint::spin_loop();
267 } else {
268 inner.parked[idx].store(true, Ordering::SeqCst);
269 if inner.epoch.load(Ordering::SeqCst) == seen
274 && !inner.shutdown.load(Ordering::Relaxed)
275 {
276 std::thread::park();
277 }
278 inner.parked[idx].store(false, Ordering::SeqCst);
279 }
280 }
281 let (task, n) = unsafe { (*inner.slot.get()).expect("job published with epoch") };
285 let f = unsafe { &*task.0 };
286 f(idx, n);
287 inner.remaining.fetch_sub(1, Ordering::AcqRel);
288 }
289}
290
291pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
294 let in_dim = x.len();
295 let out_dim = out.len();
296 debug_assert!(w.len() >= out_dim * in_dim);
297
298 let row_dot = |o: usize| -> f32 {
299 let row = &w[o * in_dim..(o + 1) * in_dim];
300 let mut sum = 0.0f32;
301 for j in 0..in_dim {
302 sum += row[j] * x[j];
303 }
304 sum
305 };
306
307 match pool {
308 Some(pool) if out_dim >= 256 => {
310 let out_addr = SendMut(out.as_mut_ptr());
311 pool.run(&move |widx, n| {
312 let chunk = out_dim.div_ceil(n);
313 let start = widx * chunk;
314 let end = (start + chunk).min(out_dim);
315 for o in start..end {
316 unsafe { *out_addr.at(o) = row_dot(o) };
318 }
319 });
320 }
321 _ => {
322 for (o, dst) in out.iter_mut().enumerate() {
323 *dst = row_dot(o);
324 }
325 }
326 }
327}
328
329pub fn matvec_rows2(
335 pool: Option<&Pool>,
336 w: &[f32],
337 x1: &[f32],
338 x2: &[f32],
339 out1: &mut [f32],
340 out2: &mut [f32],
341) {
342 let in_dim = x1.len();
343 debug_assert_eq!(x2.len(), in_dim);
344 let out_dim = out1.len();
345 debug_assert_eq!(out2.len(), out_dim);
346 debug_assert!(w.len() >= out_dim * in_dim);
347
348 let row_dots = |o: usize| -> (f32, f32) {
349 let row = &w[o * in_dim..(o + 1) * in_dim];
350 let (mut s1, mut s2) = (0.0f32, 0.0f32);
351 for j in 0..in_dim {
352 s1 += row[j] * x1[j];
353 s2 += row[j] * x2[j];
354 }
355 (s1, s2)
356 };
357
358 match pool {
359 Some(pool) if out_dim >= 256 => {
360 let o1 = SendMut(out1.as_mut_ptr());
361 let o2 = SendMut(out2.as_mut_ptr());
362 pool.run(&move |widx, n| {
363 let chunk = out_dim.div_ceil(n);
364 let start = widx * chunk;
365 let end = (start + chunk).min(out_dim);
366 for o in start..end {
367 let (s1, s2) = row_dots(o);
368 unsafe {
370 *o1.at(o) = s1;
371 *o2.at(o) = s2;
372 }
373 }
374 });
375 }
376 _ => {
377 for o in 0..out_dim {
378 let (s1, s2) = row_dots(o);
379 out1[o] = s1;
380 out2[o] = s2;
381 }
382 }
383 }
384}
385
386#[derive(Clone, Copy)]
387struct SendMut(*mut f32);
388unsafe impl Send for SendMut {}
389unsafe impl Sync for SendMut {}
390
391impl SendMut {
392 #[inline]
395 fn at(self, i: usize) -> *mut f32 {
396 unsafe { self.0.add(i) }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn parallel_matvec_equals_serial_bitexact() {
406 let (out_dim, in_dim) = (512, 64);
407 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.013).sin()).collect();
408 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
409
410 let mut serial = vec![0.0f32; out_dim];
411 matvec_rows(None, &w, &x, &mut serial);
412
413 let pool = Pool::new(4);
414 let mut parallel = vec![0.0f32; out_dim];
415 matvec_rows(Some(&pool), &w, &x, &mut parallel);
416
417 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
418 }
419
420 #[test]
421 fn fused_pair_equals_two_singles_bitexact() {
422 let (out_dim, in_dim) = (300, 48);
423 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.011).sin()).collect();
424 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
425 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
426
427 let mut a1 = vec![0.0f32; out_dim];
428 let mut a2 = vec![0.0f32; out_dim];
429 matvec_rows(None, &w, &x1, &mut a1);
430 matvec_rows(None, &w, &x2, &mut a2);
431
432 for pool in [None, Some(Pool::new(3))] {
433 let mut b1 = vec![0.0f32; out_dim];
434 let mut b2 = vec![0.0f32; out_dim];
435 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
436 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
437 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
438 }
439 }
440
441 #[test]
442 fn pool_survives_many_runs() {
443 let pool = Pool::new(3);
444 let counter = AtomicUsize::new(0);
445 for _ in 0..100 {
446 pool.run(&|_, _| {
447 counter.fetch_add(1, Ordering::Relaxed);
448 });
449 }
450 assert_eq!(counter.load(Ordering::Relaxed), 400);
452 }
453
454 #[test]
455 fn pool_wakes_after_park() {
456 let pool = Pool::with_spin(2, 0);
459 let counter = AtomicUsize::new(0);
460 for _ in 0..50 {
461 pool.run(&|_, _| {
462 counter.fetch_add(1, Ordering::Relaxed);
463 });
464 std::thread::sleep(std::time::Duration::from_micros(200));
466 }
467 assert_eq!(counter.load(Ordering::Relaxed), 150);
468 }
469
470 #[test]
471 fn worker_indices_are_distinct_and_cover_range() {
472 let pool = Pool::new(3);
473 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
474 for _ in 0..20 {
475 pool.run(&|widx, n| {
476 assert_eq!(n, 4);
477 hits[widx].fetch_add(1, Ordering::Relaxed);
478 });
479 }
480 for (i, h) in hits.iter().enumerate() {
481 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
482 }
483 }
484}