1use std::sync::Arc;
32#[cfg(any(target_os = "android", target_os = "linux"))]
33use std::sync::Mutex;
34use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
35
36pub static FORCED_THREADS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
40
41pub static WORKER_TIDS: std::sync::Mutex<Vec<i32>> = std::sync::Mutex::new(Vec::new());
46
47#[repr(align(128))]
51struct Padded<T>(T);
52
53#[repr(align(128))]
56struct Slot {
57 ticket: AtomicUsize,
63 parked: AtomicBool,
66}
67
68struct Inner {
69 epoch: AtomicUsize,
74 desc_data: AtomicUsize,
105 desc_vtable: AtomicUsize,
106 desc_n: AtomicUsize,
107 desc_dev: AtomicUsize,
108 desc_float_activations: AtomicBool,
112 remaining: Padded<AtomicUsize>,
116 slots: Box<[Slot]>,
118 shutdown: AtomicBool,
119 spin_budget: AtomicUsize,
121 oversubscribed: bool,
130 two_node: AtomicBool,
132 #[cfg(target_os = "linux")]
134 home_mask: Mutex<Vec<usize>>,
135 #[cfg(any(target_os = "android", target_os = "linux"))]
139 registered: AtomicUsize,
140 #[cfg(any(target_os = "android", target_os = "linux"))]
141 worker_tids: Mutex<Vec<i32>>,
142}
143
144static DISPATCHES: AtomicUsize = AtomicUsize::new(0);
147
148pub fn dispatch_count() -> usize {
150 DISPATCHES.load(Ordering::Relaxed)
151}
152
153pub struct Pool {
156 inner: Arc<Inner>,
157 threads: Vec<std::thread::Thread>,
159 joins: Vec<std::thread::JoinHandle<()>>,
160}
161
162fn spin_budget_from_env() -> usize {
163 std::env::var("CMF_POOL_SPIN")
164 .ok()
165 .and_then(|v| v.parse::<usize>().ok())
166 .unwrap_or(4000)
167}
168
169pub(crate) fn grain_for(rows: usize, workers: usize) -> usize {
179 if rows == 0 || workers <= 1 {
180 return rows.max(1);
181 }
182 let balanced = (rows / (workers * 8)).max(32);
183 balanced.min(rows.div_ceil(workers)).max(1)
185}
186
187impl Pool {
188 pub fn new(n_workers: usize) -> Self {
189 Self::with_spin(n_workers, spin_budget_from_env())
190 }
191
192 pub fn with_spin(n_workers: usize, spin_budget: usize) -> Self {
194 let cpus = std::thread::available_parallelism()
197 .map(|n| n.get())
198 .unwrap_or(1);
199 let inner = Arc::new(Inner {
200 epoch: AtomicUsize::new(0),
201 desc_data: AtomicUsize::new(0),
202 desc_vtable: AtomicUsize::new(0),
203 desc_n: AtomicUsize::new(0),
204 desc_dev: AtomicUsize::new(0),
205 desc_float_activations: AtomicBool::new(false),
206 remaining: Padded(AtomicUsize::new(0)),
207 slots: (0..n_workers)
208 .map(|_| Slot {
209 ticket: AtomicUsize::new(0),
210 parked: AtomicBool::new(false),
211 })
212 .collect(),
213 shutdown: AtomicBool::new(false),
214 spin_budget: AtomicUsize::new(spin_budget),
215 oversubscribed: n_workers + 1 > cpus,
216 two_node: AtomicBool::new(false),
217 #[cfg(target_os = "linux")]
218 home_mask: Mutex::new(Vec::new()),
219 #[cfg(any(target_os = "android", target_os = "linux"))]
220 registered: AtomicUsize::new(0),
221 #[cfg(any(target_os = "android", target_os = "linux"))]
222 worker_tids: Mutex::new(Vec::with_capacity(n_workers)),
223 });
224 let mut joins = Vec::with_capacity(n_workers);
225 for w in 0..n_workers {
226 let inner = inner.clone();
227 let h = std::thread::Builder::new()
228 .name(format!("cmf-pool-{w}"))
229 .spawn(move || {
230 #[cfg(any(target_os = "android", target_os = "linux"))]
231 {
232 let tid = unsafe { libc::gettid() } as i32;
233 if let Ok(mut tids) = inner.worker_tids.lock() {
234 tids.push(tid);
235 }
236 inner.registered.fetch_add(1, Ordering::Release);
237 }
238 worker_loop(&inner, w)
239 })
240 .expect("spawn pool worker");
241 joins.push(h);
242 }
243 #[cfg(any(target_os = "android", target_os = "linux"))]
250 while inner.registered.load(Ordering::Acquire) < n_workers {
251 std::thread::yield_now();
252 }
253 #[cfg(any(target_os = "android", target_os = "linux"))]
254 if let (Ok(mut global), Ok(local)) = (WORKER_TIDS.lock(), inner.worker_tids.lock()) {
255 *global = local.clone();
256 }
257 let threads = joins.iter().map(|h| h.thread().clone()).collect();
258 Self {
259 inner,
260 threads,
261 joins,
262 }
263 }
264
265 #[cfg(all(
271 target_arch = "aarch64",
272 any(target_os = "linux", target_os = "android")
273 ))]
274 fn big_cores() -> Option<usize> {
275 Self::cores_from_capacities(&core_capacities())
276 }
277
278 #[cfg_attr(
288 not(all(
289 target_arch = "aarch64",
290 any(target_os = "linux", target_os = "android")
291 )),
292 allow(dead_code)
293 )]
294 fn cores_from_capacities(caps: &[u64]) -> Option<usize> {
295 let max = *caps.iter().max()?;
296 let min = *caps.iter().min()?;
297 if caps.len() < 2 || max == min {
298 return None;
299 }
300 Some(caps.iter().filter(|&&c| c * 8 >= max * 5).count())
301 }
302
303 #[cfg(target_os = "macos")]
304 fn big_cores() -> Option<usize> {
305 if true {
313 return None;
314 }
315 #[allow(unreachable_code)]
316 unsafe extern "C" {
317 fn sysctlbyname(
318 name: *const std::ffi::c_char,
319 oldp: *mut std::ffi::c_void,
320 oldlenp: *mut usize,
321 newp: *mut std::ffi::c_void,
322 newlen: usize,
323 ) -> std::ffi::c_int;
324 }
325 unsafe {
326 let name = std::ffi::CString::new("hw.perflevel0.physicalcpu").ok()?;
327 let mut count: i32 = 0;
328 let mut size = std::mem::size_of::<i32>();
329 let ret = sysctlbyname(
330 name.as_ptr(),
331 &mut count as *mut i32 as *mut std::ffi::c_void,
332 &mut size,
333 std::ptr::null_mut(),
334 0,
335 );
336 if ret == 0 && count > 0 {
337 Some(count as usize)
338 } else {
339 None
340 }
341 }
342 }
343
344 #[cfg(not(any(
345 all(
346 target_arch = "aarch64",
347 any(target_os = "linux", target_os = "android")
348 ),
349 target_os = "macos"
350 )))]
351 fn big_cores() -> Option<usize> {
352 None
353 }
354
355 pub fn effective_threads() -> usize {
361 let forced = FORCED_THREADS.load(std::sync::atomic::Ordering::Relaxed);
362 if forced > 0 {
363 return forced;
364 }
365 match std::env::var("CMF_THREADS") {
366 Ok(v) => v.parse::<usize>().unwrap_or(0),
367 Err(_) => match Self::big_cores() {
368 Some(big) => big,
369 None => {
370 let avail = std::thread::available_parallelism()
380 .map(|n| n.get())
381 .unwrap_or(1);
382 avail.saturating_sub(1).min(32)
383 }
384 },
385 }
386 }
387
388 pub fn from_env() -> Option<Arc<Self>> {
391 let n = Self::effective_threads();
392 if n <= 1 {
393 None
394 } else {
395 Some(Arc::new(Self::new(n)))
396 }
397 }
398
399 pub fn n_workers(&self) -> usize {
401 self.threads.len()
402 }
403
404 pub fn bind_numa(&self, regions: &[&[u8]]) {
422 #[cfg(target_os = "linux")]
423 {
424 let Some((node, cpus)) = numa::choose(regions, self.threads.len() + 1) else {
425 return;
426 };
427 let mut applied = 0usize;
428 if let Ok(tids) = self.inner.worker_tids.lock() {
429 for &tid in tids.iter() {
430 if numa::set_affinity(tid, &cpus) {
431 applied += 1;
432 }
433 }
434 }
435 numa::publish(cpus.clone());
436 numa::adopt_caller();
437 tracing::info!(
438 "numa: pool bound to node {node} ({} cpus, {applied}/{} workers)",
439 cpus.len(),
440 self.threads.len()
441 );
442 if std::env::var("CMF_NUMA_TRACE").is_ok_and(|v| v != "0") {
443 eprintln!(
444 "numa: pool bound to node {node}: {} cpus, {applied}/{} workers",
445 cpus.len(),
446 self.threads.len()
447 );
448 }
449 }
450 #[cfg(not(target_os = "linux"))]
451 let _ = regions;
452 }
453
454 pub fn two_nodes(&self) -> bool {
456 self.inner.two_node.load(Ordering::Relaxed)
457 }
458
459 pub fn far_worker(&self, w: usize) -> bool {
462 w % 2 == 1 && w < self.threads.len() && self.two_nodes()
463 }
464
465 pub fn bind_two_nodes(&self, hint: Option<usize>) -> Option<(usize, usize, usize, usize)> {
481 #[cfg(target_os = "linux")]
482 {
483 let nw = self.threads.len();
484 let far_n = nw / 2;
485 if far_n < 2 || self.two_nodes() {
486 return None;
487 }
488 let nodes = numa::nodes();
489 if nodes.len() < 2 {
490 return None;
491 }
492 let before = numa::allowed();
493 let mut home_mask = numa::published();
494 let fresh = home_mask.is_empty();
495 let home = if fresh {
496 let home = hint?;
497 home_mask = numa::node_mask(&nodes, home, nw + 1)?;
498 home
499 } else {
500 nodes.iter().find(|(_, cpus)| home_mask.iter().all(|c| cpus.contains(c)))?.0
501 };
502 let mut best: Option<(usize, Vec<usize>)> = None;
504 for (id, cpus) in nodes.iter().filter(|(id, _)| *id != home) {
505 let prim: Vec<usize> = cpus.iter().copied().filter(|&c| numa::primary(c)).collect();
506 let set = if prim.len() >= far_n { prim } else { cpus.clone() };
507 if set.len() >= far_n && best.as_ref().is_none_or(|b| set.len() > b.1.len()) {
508 best = Some((*id, set));
509 }
510 }
511 let (far, far_cpus) = best?;
512 let moved = AtomicUsize::new(0);
514 self.run(&|w, _n| {
515 if w >= nw {
516 return;
517 }
518 if w % 2 == 0 {
519 numa::set_affinity(0, &home_mask);
521 } else if numa::set_affinity(0, &far_cpus) {
522 let now = numa::allowed();
523 if !now.is_empty() && now.iter().all(|c| far_cpus.contains(c)) {
524 moved.fetch_add(1, Ordering::Relaxed);
525 }
526 }
527 });
528 if moved.load(Ordering::Relaxed) != far_n {
529 let back = if fresh { &before } else { &home_mask };
530 self.run(&|w, _n| {
531 if w < nw && (fresh || w % 2 == 1) {
532 numa::set_affinity(0, back);
533 }
534 });
535 return None;
536 }
537 if fresh {
538 numa::publish(home_mask.clone());
539 numa::adopt_caller();
540 }
541 if let Ok(mut m) = self.inner.home_mask.lock() {
542 *m = home_mask;
543 }
544 self.inner.two_node.store(true, Ordering::Relaxed);
545 tracing::info!("numa: pool on two nodes: {} threads on node {home}, {far_n} on node {far}", nw - far_n + 1);
546 Some((home, far, nw - far_n + 1, far_n))
547 }
548 #[cfg(not(target_os = "linux"))]
549 None
550 }
551
552 #[cfg(test)]
555 pub(crate) fn mark_two_nodes(&self, on: bool) {
556 self.inner.two_node.store(on, Ordering::Relaxed);
557 }
558
559 pub fn unbind_two_nodes(&self) {
561 if !self.inner.two_node.swap(false, Ordering::Relaxed) {
562 return;
563 }
564 #[cfg(target_os = "linux")]
565 {
566 let nw = self.threads.len();
567 let home = self.inner.home_mask.lock().map(|m| m.clone()).unwrap_or_default();
568 if !home.is_empty() {
569 self.run(&|w, _n| {
570 if w % 2 == 1 && w < nw {
571 numa::set_affinity(0, &home);
572 }
573 });
574 }
575 }
576 }
577
578 pub fn bind_node(&self, node: usize) {
582 #[cfg(target_os = "linux")]
583 {
584 let mut mask = numa::published();
585 if mask.is_empty() {
586 let Some(m) = numa::node_mask(&numa::nodes(), node, self.threads.len() + 1) else {
587 return;
588 };
589 numa::publish(m.clone());
590 mask = m;
591 }
592 if let Ok(tids) = self.inner.worker_tids.lock() {
593 for &tid in tids.iter() {
594 numa::set_affinity(tid, &mask);
595 }
596 }
597 numa::adopt_caller();
598 }
599 #[cfg(not(target_os = "linux"))]
600 let _ = node;
601 }
602
603 pub fn place_file_pages(
624 &self,
625 home: &[&[u8]],
626 far: &[&[u8]],
627 nodes: (usize, usize),
628 fill: usize,
629 on_move: &dyn Fn(usize),
630 ) -> Option<NodePlacement> {
631 #[cfg(target_os = "linux")]
632 {
633 if !self.two_nodes() {
634 return None;
635 }
636 let mut files = Vec::new();
637 let spans = [numa::file_spans(home, &mut files)?, numa::file_spans(far, &mut files)?];
638 let node = [nodes.0, nodes.1];
639 let mut out = NodePlacement::default();
640 let filled = AtomicUsize::new(0);
642 self.run_sides([spans[0].len().min(fill), spans[1].len().min(fill)], &|s, i| {
643 let sp = &spans[s][i];
644 if !numa::cached(sp) {
645 numa::read_in(sp);
646 filled.fetch_add(sp.len, Ordering::Relaxed);
647 }
648 });
649 out.filled = filled.load(Ordering::Relaxed);
650 let mut share = [0.0f64; 2];
651 for s in 0..2 {
652 share[s] = self.side_share(&spans[s], s == 1, node[s], 0)?;
653 }
654 let todo: [Vec<usize>; 2] = [0, 1].map(|s| {
656 if share[s] >= 0.9 {
657 return Vec::new();
658 }
659 (0..spans[s].len()).filter(|&i| numa::cached(&spans[s][i])).collect()
660 });
661 out.moved = (0..2).map(|s| todo[s].iter().map(|&i| spans[s][i].len).sum::<usize>()).sum();
662 if out.moved > 0 {
663 on_move(out.moved);
664 for s in 0..2 {
665 for &i in &todo[s] {
666 numa::drop_cached(&spans[s][i]);
667 }
668 }
669 self.run_sides([todo[0].len(), todo[1].len()], &|s, i| numa::read_in(&spans[s][todo[s][i]]));
670 for s in 0..2 {
671 if !todo[s].is_empty() {
672 share[s] = self.side_share(&spans[s], s == 1, node[s], 1)?;
673 }
674 }
675 }
676 (out.home, out.far) = (share[0], share[1]);
677 Some(out)
678 }
679 #[cfg(not(target_os = "linux"))]
680 {
681 let _ = (home, far, nodes, fill, on_move);
682 None
683 }
684 }
685
686 #[cfg(target_os = "linux")]
689 fn run_sides(&self, n: [usize; 2], f: &(dyn Fn(usize, usize) + Sync)) {
690 let cur = [AtomicUsize::new(0), AtomicUsize::new(0)];
691 self.run(&|w, _n| {
692 let s = usize::from(self.far_worker(w));
693 loop {
694 let i = cur[s].fetch_add(1, Ordering::Relaxed);
695 if i >= n[s] {
696 break;
697 }
698 f(s, i);
699 }
700 });
701 }
702
703 #[cfg(target_os = "linux")]
707 fn side_share(&self, spans: &[numa::Span], far: bool, node: usize, salt: usize) -> Option<f64> {
708 let side = usize::from(far);
709 let by = numa::sample_nodes(spans, salt, &|maps| {
710 let mut n = [0usize; 2];
711 n[side] = maps.len();
712 self.run_sides(n, &|_s, i| numa::touch(maps[i].0, maps[i].1));
713 });
714 let total: usize = by.iter().sum();
715 (total > 0).then(|| by.get(node).copied().unwrap_or(0) as f64 / total as f64)
716 }
717
718 pub(crate) fn set_spin_budget(&self, spins: usize) {
722 self.inner.spin_budget.store(spins, Ordering::Relaxed);
723 }
724
725 fn dispatch(&self, f: &(dyn Fn(usize, usize) + Sync), nw: usize) {
732 let _tot = crate::cpuprof::time(crate::cpuprof::Slot::DispTotal);
733 let mut _setup = crate::cpuprof::time(crate::cpuprof::Slot::DispSetup);
734 let inner = &*self.inner;
735 let n = nw + 1;
736 let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
737 let raw: [usize; 2] = unsafe { std::mem::transmute(ptr) };
740 inner.desc_data.store(raw[0], Ordering::Relaxed);
741 inner.desc_vtable.store(raw[1], Ordering::Relaxed);
742 inner.desc_n.store(n, Ordering::Relaxed);
743 inner
744 .desc_dev
745 .store(crate::gpu::current_device(), Ordering::Relaxed);
746 inner.desc_float_activations.store(
747 crate::qtensor::float_activations_active(),
748 Ordering::Relaxed,
749 );
750 inner.remaining.0.store(nw, Ordering::Relaxed);
751 let e = inner.epoch.load(Ordering::Relaxed);
752 inner.epoch.store(e.wrapping_add(1), Ordering::Relaxed);
753 let slots = &inner.slots[..nw];
756 for slot in slots {
757 let t = slot.ticket.load(Ordering::Relaxed).wrapping_add(1);
758 slot.ticket.store(t, Ordering::Release);
759 }
760 std::sync::atomic::fence(Ordering::SeqCst);
765 for (slot, t) in slots.iter().zip(&self.threads) {
766 if slot.parked.load(Ordering::Relaxed) {
767 t.unpark();
768 }
769 }
770 drop(_setup);
771
772 let _work = crate::cpuprof::time(crate::cpuprof::Slot::DispWork);
775 f(nw, n);
776 drop(_work);
777
778 let _wait = crate::cpuprof::time(crate::cpuprof::Slot::DispWait);
782 let spin_limit = if inner.oversubscribed { 64 } else { 10_000 };
783 let mut spins = 0usize;
784 while inner.remaining.0.load(Ordering::Acquire) != 0 {
785 spins += 1;
786 if spins < spin_limit {
787 std::hint::spin_loop();
788 } else {
789 std::thread::yield_now();
790 }
791 }
792 drop(_wait);
793 drop(_tot);
794 }
795
796 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
807 let grain = grain_for(rows, self.threads.len() + 1);
808 let chunks = rows.div_ceil(grain.max(1));
809 let next = AtomicUsize::new(0);
810 self.run_limited(chunks, &|_w, _n| loop {
811 let start = next.fetch_add(grain, Ordering::Relaxed);
812 if start >= rows {
813 break;
814 }
815 f(start, (start + grain).min(rows));
816 });
817 }
818
819 fn run_limited(&self, max_workers: usize, f: &(dyn Fn(usize, usize) + Sync)) {
829 let nw = self.threads.len().min(max_workers);
830 if nw == self.threads.len() {
831 return self.run(f);
832 }
833 #[cfg(target_os = "linux")]
834 numa::adopt_caller();
835 DISPATCHES.fetch_add(1, Ordering::Relaxed);
836 self.dispatch(f, nw);
838 }
839
840 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
848 let total: usize = parts.iter().map(|p| p.0).sum();
849 if total == 0 {
850 return;
851 }
852 let grain = grain_for(total, self.threads.len() + 1);
853 let chunks = total.div_ceil(grain.max(1));
854 let next = AtomicUsize::new(0);
855 self.run_limited(chunks, &|_w, _n| loop {
856 let s = next.fetch_add(grain, Ordering::Relaxed);
857 if s >= total {
858 break;
859 }
860 let e = (s + grain).min(total);
861 let mut base = 0usize;
862 for &(rows, f) in parts {
863 let a = s.max(base);
864 let b = e.min(base + rows);
865 if a < b {
866 f(a - base, b - base);
867 }
868 base += rows;
869 if base >= e {
870 break;
871 }
872 }
873 });
874 }
875
876 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
880 #[cfg(target_os = "linux")]
881 numa::adopt_caller();
882 DISPATCHES.fetch_add(1, Ordering::Relaxed);
883 self.dispatch(f, self.threads.len());
884 }
885}
886
887impl Drop for Pool {
888 fn drop(&mut self) {
889 self.inner.shutdown.store(true, Ordering::SeqCst);
890 for t in &self.threads {
891 t.unpark();
892 }
893 for h in self.joins.drain(..) {
894 let _ = h.join();
895 }
896 }
897}
898
899#[derive(Clone, Copy, Debug, Default)]
901pub struct NodePlacement {
902 pub home: f64,
904 pub far: f64,
906 pub moved: usize,
908 pub filled: usize,
910}
911
912pub fn file_pages_node(regions: &[&[u8]]) -> Option<usize> {
916 #[cfg(target_os = "linux")]
917 {
918 let mut files = Vec::new();
919 let spans = numa::file_spans(regions, &mut files)?;
920 let by = numa::sample_nodes(&spans, 0, &|maps| {
921 for &(a, l) in maps {
922 numa::touch(a, l);
923 }
924 });
925 let (node, &pages) = by.iter().enumerate().max_by_key(|(_, c)| **c)?;
926 (pages > 0).then_some(node)
927 }
928 #[cfg(not(target_os = "linux"))]
929 {
930 let _ = regions;
931 None
932 }
933}
934
935#[cfg(target_os = "linux")]
937mod numa {
938 use std::sync::Mutex;
939 use std::sync::atomic::{AtomicUsize, Ordering};
940
941 static MASK: Mutex<Vec<usize>> = Mutex::new(Vec::new());
944 static EPOCH: AtomicUsize = AtomicUsize::new(0);
945 thread_local! {
946 static SEEN: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
947 }
948
949 pub(super) fn publish(cpus: Vec<usize>) {
950 if let Ok(mut m) = MASK.lock() {
951 *m = cpus;
952 }
953 EPOCH.fetch_add(1, Ordering::Release);
954 }
955
956 #[inline]
958 pub(super) fn adopt_caller() {
959 let e = EPOCH.load(Ordering::Acquire);
960 if e == 0 || SEEN.with(|c| c.get()) == e {
961 return;
962 }
963 SEEN.with(|c| c.set(e));
964 if let Ok(m) = MASK.lock() {
965 if !m.is_empty() {
966 set_affinity(0, &m);
967 }
968 }
969 }
970
971 pub(super) fn published() -> Vec<usize> {
973 MASK.lock().map(|m| m.clone()).unwrap_or_default()
974 }
975
976 pub(super) fn parse_list(s: &str) -> Vec<usize> {
977 let mut out = Vec::new();
978 for part in s.trim().split(',') {
979 let part = part.trim();
980 if part.is_empty() {
981 continue;
982 }
983 match part.split_once('-') {
984 Some((a, b)) => {
985 if let (Ok(a), Ok(b)) = (a.parse::<usize>(), b.parse::<usize>()) {
986 out.extend(a..=b);
987 }
988 }
989 None => {
990 if let Ok(a) = part.parse() {
991 out.push(a);
992 }
993 }
994 }
995 }
996 out
997 }
998
999 pub(super) fn nodes() -> Vec<(usize, Vec<usize>)> {
1000 let mut v = Vec::new();
1001 let Ok(rd) = std::fs::read_dir("/sys/devices/system/node") else {
1002 return v;
1003 };
1004 for e in rd.flatten() {
1005 let name = e.file_name().to_string_lossy().to_string();
1006 let Some(id) = name.strip_prefix("node").and_then(|x| x.parse::<usize>().ok()) else {
1007 continue;
1008 };
1009 if let Ok(l) = std::fs::read_to_string(e.path().join("cpulist")) {
1010 let cpus = parse_list(&l);
1011 if !cpus.is_empty() {
1012 v.push((id, cpus));
1013 }
1014 }
1015 }
1016 v.sort();
1017 v
1018 }
1019
1020 pub(super) fn allowed() -> Vec<usize> {
1021 unsafe {
1023 let mut set: libc::cpu_set_t = std::mem::zeroed();
1024 if libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut set) != 0 {
1025 return Vec::new();
1026 }
1027 (0..libc::CPU_SETSIZE as usize)
1028 .filter(|&c| libc::CPU_ISSET(c, &set))
1029 .collect()
1030 }
1031 }
1032
1033 pub(super) fn primary(cpu: usize) -> bool {
1035 let p = format!("/sys/devices/system/cpu/cpu{cpu}/topology/thread_siblings_list");
1036 match std::fs::read_to_string(p) {
1037 Ok(l) => parse_list(&l).first().is_none_or(|&f| f == cpu),
1038 Err(_) => true,
1039 }
1040 }
1041
1042 fn page_nodes(regions: &[&[u8]]) -> (usize, usize, Vec<usize>) {
1052 const PAGE: usize = 4096;
1053 let total: usize = regions.iter().map(|r| r.len() / PAGE).sum();
1054 if total == 0 {
1055 return (0, 0, Vec::new());
1056 }
1057 let stride = total.div_ceil(4096).max(1);
1058 let mut pages: Vec<*mut libc::c_void> = Vec::new();
1059 for r in regions {
1060 let base = (r.as_ptr() as usize).div_ceil(PAGE) * PAGE;
1061 let end = r.as_ptr() as usize + r.len();
1062 let mut a = base;
1063 while a + PAGE <= end {
1064 pages.push(a as *mut libc::c_void);
1065 a += PAGE * stride;
1066 }
1067 }
1068 let mut incore = 0usize;
1069 for &p in &pages {
1070 let mut vec = 0u8;
1071 let cached = unsafe { libc::mincore(p, PAGE, &mut vec) } == 0 && vec & 1 == 1;
1073 if cached {
1074 incore += 1;
1075 unsafe { std::ptr::read_volatile(p as *const u8) };
1077 }
1078 }
1079 let mut status = vec![-1i32; pages.len()];
1080 let rc = unsafe {
1083 libc::syscall(
1084 libc::SYS_move_pages,
1085 0,
1086 pages.len() as libc::c_ulong,
1087 pages.as_mut_ptr(),
1088 std::ptr::null::<libc::c_int>(),
1089 status.as_mut_ptr(),
1090 0,
1091 )
1092 };
1093 let mut by = Vec::new();
1094 if rc == 0 {
1095 for &st in &status {
1096 if st >= 0 {
1097 let n = st as usize;
1098 if by.len() <= n {
1099 by.resize(n + 1, 0);
1100 }
1101 by[n] += 1;
1102 }
1103 }
1104 } else {
1105 by = numa_maps_nodes(regions);
1106 }
1107 (pages.len(), incore, by)
1108 }
1109
1110 pub(super) fn numa_maps_nodes(regions: &[&[u8]]) -> Vec<usize> {
1113 let (Ok(maps), Ok(nm)) = (
1114 std::fs::read_to_string("/proc/self/maps"),
1115 std::fs::read_to_string("/proc/self/numa_maps"),
1116 ) else {
1117 return Vec::new();
1118 };
1119 let spans: Vec<(usize, usize)> = regions
1120 .iter()
1121 .map(|r| (r.as_ptr() as usize, r.as_ptr() as usize + r.len()))
1122 .collect();
1123 let mut starts = std::collections::HashSet::new();
1124 for line in maps.lines() {
1125 let Some((range, _)) = line.split_once(' ') else {
1126 continue;
1127 };
1128 let Some((a, b)) = range.split_once('-') else {
1129 continue;
1130 };
1131 let (Ok(a), Ok(b)) = (usize::from_str_radix(a, 16), usize::from_str_radix(b, 16)) else {
1132 continue;
1133 };
1134 if spans.iter().any(|&(s, e)| s < b && a < e) {
1135 starts.insert(a);
1136 }
1137 }
1138 let mut by = Vec::new();
1139 for line in nm.lines() {
1140 let mut it = line.split_whitespace();
1141 let Some(a) = it.next().and_then(|a| usize::from_str_radix(a, 16).ok()) else {
1142 continue;
1143 };
1144 if !starts.contains(&a) {
1145 continue;
1146 }
1147 for f in it {
1148 let Some((k, v)) = f.split_once('=') else {
1149 continue;
1150 };
1151 let (Some(n), Ok(v)) = (
1152 k.strip_prefix('N').and_then(|n| n.parse::<usize>().ok()),
1153 v.parse::<usize>(),
1154 ) else {
1155 continue;
1156 };
1157 if by.len() <= n {
1158 by.resize(n + 1, 0);
1159 }
1160 by[n] += v;
1161 }
1162 }
1163 by
1164 }
1165
1166 const PAGE: usize = 4096;
1167
1168 pub(super) struct Span {
1171 pub addr: usize,
1172 pub len: usize,
1173 fd: i32,
1174 off: i64,
1175 }
1176
1177 pub(super) fn file_spans(regions: &[&[u8]], files: &mut Vec<(String, std::fs::File)>) -> Option<Vec<Span>> {
1182 use std::os::fd::AsRawFd;
1183 use std::os::unix::fs::FileExt;
1184 let maps = std::fs::read_to_string("/proc/self/maps").ok()?;
1185 let mut vmas: Vec<(usize, usize, u64, &str)> = Vec::new();
1187 for line in maps.lines() {
1188 let mut it = line.splitn(6, ' ');
1189 let (Some(range), Some(_), Some(off), Some(_), Some(_), Some(path)) =
1190 (it.next(), it.next(), it.next(), it.next(), it.next(), it.next())
1191 else {
1192 continue;
1193 };
1194 let path = path.trim();
1195 let Some((a, b)) = range.split_once('-') else {
1196 continue;
1197 };
1198 let (Ok(a), Ok(b), Ok(off)) = (usize::from_str_radix(a, 16), usize::from_str_radix(b, 16), u64::from_str_radix(off, 16))
1199 else {
1200 continue;
1201 };
1202 if path.starts_with('/') {
1203 vmas.push((a, b, off, path));
1204 }
1205 }
1206 let mut out = Vec::with_capacity(regions.len());
1207 for r in regions {
1208 let a = (r.as_ptr() as usize).div_ceil(PAGE) * PAGE;
1209 let e = (r.as_ptr() as usize + r.len()) / PAGE * PAGE;
1210 if e <= a {
1211 continue;
1212 }
1213 let Some(v) = vmas.get(vmas.partition_point(|v| v.1 <= a)).filter(|v| v.0 <= a && e <= v.1) else {
1214 continue;
1215 };
1216 let off = v.2 + (a - v.0) as u64;
1217 let fi = match files.iter().position(|f| f.0 == v.3) {
1218 Some(fi) => fi,
1219 None => {
1220 let f = std::fs::File::open(v.3).ok()?;
1221 let mut page = [0u8; PAGE];
1223 f.read_exact_at(&mut page, off).ok()?;
1224 if page[..] != unsafe { std::slice::from_raw_parts(a as *const u8, PAGE) }[..] {
1226 return None;
1227 }
1228 files.push((v.3.to_string(), f));
1229 files.len() - 1
1230 }
1231 };
1232 out.push(Span { addr: a, len: e - a, fd: files[fi].1.as_raw_fd(), off: off as i64 });
1233 }
1234 (!out.is_empty()).then_some(out)
1235 }
1236
1237 pub(super) fn cached(s: &Span) -> bool {
1239 let mut v = vec![0u8; s.len / PAGE];
1240 unsafe { libc::mincore(s.addr as *mut libc::c_void, s.len, v.as_mut_ptr()) == 0 && v.iter().all(|b| b & 1 == 1) }
1242 }
1243
1244 pub(super) fn drop_cached(s: &Span) {
1247 unsafe {
1249 libc::madvise(s.addr as *mut libc::c_void, s.len, libc::MADV_DONTNEED);
1250 libc::posix_fadvise(s.fd, s.off as libc::off_t, s.len as libc::off_t, libc::POSIX_FADV_DONTNEED);
1251 }
1252 }
1253
1254 pub(super) fn read_in(s: &Span) {
1257 unsafe { libc::madvise(s.addr as *mut libc::c_void, s.len, libc::MADV_WILLNEED) };
1259 touch(s.addr, s.len);
1260 }
1261
1262 pub(super) fn touch(addr: usize, len: usize) {
1264 let mut a = addr;
1265 while a < addr + len {
1266 unsafe { std::ptr::read_volatile(a as *const u8) };
1268 a += PAGE;
1269 }
1270 }
1271
1272 pub(super) fn sample_nodes(spans: &[Span], salt: usize, touch_all: &dyn Fn(&[(usize, usize)])) -> Vec<usize> {
1278 let step = spans.len().div_ceil(192).max(1);
1279 let mut maps: Vec<(usize, usize)> = Vec::new();
1280 for s in spans.iter().skip(salt * (step / 2) % step).step_by(step) {
1281 let p = unsafe { libc::mmap(std::ptr::null_mut(), s.len, libc::PROT_READ, libc::MAP_SHARED, s.fd, s.off as libc::off_t) };
1283 if p != libc::MAP_FAILED {
1284 maps.push((p as usize, s.len));
1285 }
1286 }
1287 touch_all(&maps);
1288 let views: Vec<&[u8]> = maps.iter().map(|&(a, l)| unsafe { std::slice::from_raw_parts(a as *const u8, l) }).collect();
1290 let by = numa_maps_nodes(&views);
1291 drop(views);
1292 for &(a, l) in &maps {
1293 unsafe { libc::munmap(a as *mut libc::c_void, l) };
1295 }
1296 by
1297 }
1298
1299 pub(super) fn node_mask(nodes: &[(usize, Vec<usize>)], node: usize, threads: usize) -> Option<Vec<usize>> {
1303 let cpus = &nodes.iter().find(|(id, _)| *id == node)?.1;
1304 let allowed = allowed();
1305 let usable: Vec<usize> = cpus.iter().copied().filter(|c| allowed.contains(c)).collect();
1306 let prim: Vec<usize> = usable.iter().copied().filter(|&c| primary(c)).collect();
1307 if prim.len() >= threads {
1308 Some(prim)
1309 } else if usable.len() >= threads {
1310 Some(usable)
1311 } else {
1312 None
1313 }
1314 }
1315
1316 pub(super) fn choose(regions: &[&[u8]], threads: usize) -> Option<(usize, Vec<usize>)> {
1318 let env = std::env::var("CMF_NUMA").ok();
1319 if matches!(env.as_deref(), Some("0") | Some("off")) {
1320 return None;
1321 }
1322 let trace = std::env::var("CMF_NUMA_TRACE").is_ok_and(|v| v != "0");
1323 let nodes = nodes();
1324 if nodes.len() < 2 {
1325 if trace {
1326 eprintln!("numa: {} node(s) visible — nothing to bind", nodes.len());
1327 }
1328 return None;
1329 }
1330 let forced = env
1332 .as_deref()
1333 .and_then(|v| v.strip_prefix("node:"))
1334 .and_then(|v| v.parse::<usize>().ok());
1335 let node = match forced {
1336 Some(n) => n,
1337 None => {
1338 let (sampled, incore, by) = page_nodes(regions);
1343 let resident: usize = by.iter().sum();
1344 if trace {
1345 eprintln!(
1346 "numa: sampled {sampled} weight pages, {incore} cached, mapped by node {by:?}"
1347 );
1348 }
1349 if sampled == 0 || incore * 2 < sampled || resident == 0 {
1350 return None;
1351 }
1352 let (n, &cnt) = by.iter().enumerate().max_by_key(|(_, c)| **c)?;
1353 if cnt * 10 < resident * 9 {
1354 return None;
1355 }
1356 n
1357 }
1358 };
1359 Some((node, node_mask(&nodes, node, threads)?))
1360 }
1361
1362 pub(super) fn set_affinity(tid: i32, cpus: &[usize]) -> bool {
1364 unsafe {
1366 let mut set: libc::cpu_set_t = std::mem::zeroed();
1367 for &c in cpus {
1368 if c < libc::CPU_SETSIZE as usize {
1369 libc::CPU_SET(c, &mut set);
1370 }
1371 }
1372 libc::sched_setaffinity(tid, std::mem::size_of::<libc::cpu_set_t>(), &set) == 0
1373 }
1374 }
1375}
1376
1377#[cfg(any(
1382 target_os = "android",
1383 all(target_arch = "aarch64", target_os = "linux")
1384))]
1385fn core_capacities() -> Vec<u64> {
1386 let read_all = |leaf: &str| -> Vec<u64> {
1387 let mut vals = Vec::new();
1388 for cpu in 0.. {
1389 let path = format!("/sys/devices/system/cpu/cpu{cpu}/{leaf}");
1390 match std::fs::read_to_string(&path) {
1391 Ok(v) => match v.trim().parse() {
1392 Ok(x) => vals.push(x),
1393 Err(_) => break,
1394 },
1395 Err(_) => break,
1396 }
1397 }
1398 vals
1399 };
1400 let caps = read_all("cpu_capacity");
1401 if caps.len() >= 2 {
1402 return caps;
1403 }
1404 read_all("cpufreq/cpuinfo_max_freq")
1405}
1406
1407#[cfg(target_os = "android")]
1408fn pin_thread_to_big_cores() {
1409 use std::mem;
1410 let caps = core_capacities();
1411 let max = caps.iter().copied().max().unwrap_or(0);
1412 let min = caps.iter().copied().min().unwrap_or(0);
1413
1414 if caps.len() < 2 || max == min {
1416 return;
1417 }
1418
1419 unsafe {
1420 let mut set: libc::cpu_set_t = mem::zeroed();
1421 for (i, &c) in caps.iter().enumerate() {
1422 if c * 8 >= max * 5 {
1423 libc::CPU_SET(i, &mut set);
1424 }
1425 }
1426 libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &set);
1427 }
1428}
1429
1430fn worker_loop(inner: &Inner, idx: usize) {
1431 #[cfg(target_os = "android")]
1432 pin_thread_to_big_cores();
1433 #[cfg(target_os = "macos")]
1440 unsafe {
1441 libc::pthread_set_qos_class_self_np(libc::qos_class_t::QOS_CLASS_USER_INITIATED, 0);
1442 }
1443
1444 let slot = &inner.slots[idx];
1445 let mut seen = 0usize;
1450 let mut seen_epoch = 0usize;
1451 loop {
1452 let mut spins = 0usize;
1455 loop {
1456 let t = slot.ticket.load(Ordering::Acquire);
1457 if t != seen {
1458 seen = t;
1459 break;
1460 }
1461 if inner.shutdown.load(Ordering::Relaxed) {
1462 return;
1463 }
1464 if !inner.oversubscribed {
1465 let e = inner.epoch.load(Ordering::Relaxed);
1468 if e != seen_epoch {
1469 seen_epoch = e;
1470 spins = 0;
1471 }
1472 }
1473 if spins < inner.spin_budget.load(Ordering::Relaxed) {
1474 spins += 1;
1475 if inner.oversubscribed && spins.is_multiple_of(64) {
1476 std::thread::yield_now();
1477 } else {
1478 std::hint::spin_loop();
1479 }
1480 } else {
1481 slot.parked.store(true, Ordering::Relaxed);
1482 std::sync::atomic::fence(Ordering::SeqCst);
1487 if slot.ticket.load(Ordering::Relaxed) == seen
1488 && !inner.shutdown.load(Ordering::Relaxed)
1489 {
1490 std::thread::park();
1491 }
1492 slot.parked.store(false, Ordering::Relaxed);
1493 }
1494 }
1495 let data = inner.desc_data.load(Ordering::Relaxed);
1499 let vtable = inner.desc_vtable.load(Ordering::Relaxed);
1500 let n = inner.desc_n.load(Ordering::Relaxed);
1501 let dev = inner.desc_dev.load(Ordering::Relaxed);
1502 let float_activations = inner.desc_float_activations.load(Ordering::Relaxed);
1503 let task: *const (dyn Fn(usize, usize) + Sync + 'static) =
1507 unsafe { std::mem::transmute([data, vtable]) };
1508 let f = unsafe { &*task };
1509 crate::gpu::set_current_device(dev);
1510 let _float_activations =
1511 float_activations.then(crate::qtensor::enter_float_activations_scope);
1512 f(idx, n);
1513 inner.remaining.0.fetch_sub(1, Ordering::AcqRel);
1514 }
1515}
1516
1517pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
1520 let in_dim = x.len();
1521 let out_dim = out.len();
1522 debug_assert!(w.len() >= out_dim * in_dim);
1523
1524 let row_dot = |o: usize| -> f32 {
1525 let row = &w[o * in_dim..(o + 1) * in_dim];
1526 let mut sum = 0.0f32;
1527 for j in 0..in_dim {
1528 sum += row[j] * x[j];
1529 }
1530 sum
1531 };
1532
1533 let out_addr = SendMut(out.as_mut_ptr());
1534 let run_range = move |start: usize, end: usize| {
1535 let mut o = start;
1536 while end - o >= 4 {
1540 let base = o * in_dim;
1541 let w0 = &w[base..base + in_dim];
1542 let w1 = &w[base + in_dim..base + 2 * in_dim];
1543 let w2 = &w[base + 2 * in_dim..base + 3 * in_dim];
1544 let w3 = &w[base + 3 * in_dim..base + 4 * in_dim];
1545 let (mut a, mut b, mut c, mut d) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
1546 for j in 0..in_dim {
1547 let v = x[j];
1548 a += w0[j] * v;
1549 b += w1[j] * v;
1550 c += w2[j] * v;
1551 d += w3[j] * v;
1552 }
1553 unsafe {
1555 *out_addr.at(o) = a;
1556 *out_addr.at(o + 1) = b;
1557 *out_addr.at(o + 2) = c;
1558 *out_addr.at(o + 3) = d;
1559 }
1560 o += 4;
1561 }
1562 for o in o..end {
1563 unsafe { *out_addr.at(o) = row_dot(o) };
1564 }
1565 };
1566 match pool {
1567 Some(pool) if out_dim >= 256 => pool.run_rows(out_dim, &run_range),
1568 _ => run_range(0, out_dim),
1569 }
1570}
1571
1572pub fn matvec_rows2(
1578 pool: Option<&Pool>,
1579 w: &[f32],
1580 x1: &[f32],
1581 x2: &[f32],
1582 out1: &mut [f32],
1583 out2: &mut [f32],
1584) {
1585 let in_dim = x1.len();
1586 debug_assert_eq!(x2.len(), in_dim);
1587 let out_dim = out1.len();
1588 debug_assert_eq!(out2.len(), out_dim);
1589 debug_assert!(w.len() >= out_dim * in_dim);
1590
1591 let row_dots = |o: usize| -> (f32, f32) {
1592 let row = &w[o * in_dim..(o + 1) * in_dim];
1593 let (mut s1, mut s2) = (0.0f32, 0.0f32);
1594 for j in 0..in_dim {
1595 s1 += row[j] * x1[j];
1596 s2 += row[j] * x2[j];
1597 }
1598 (s1, s2)
1599 };
1600
1601 match pool {
1602 Some(pool) if out_dim >= 256 => {
1603 let o1 = SendMut(out1.as_mut_ptr());
1604 let o2 = SendMut(out2.as_mut_ptr());
1605 let run_range = move |start: usize, end: usize| {
1606 for o in start..end {
1607 let (s1, s2) = row_dots(o);
1608 unsafe {
1609 *o1.at(o) = s1;
1610 *o2.at(o) = s2;
1611 }
1612 }
1613 };
1614 pool.run_rows(out_dim, &run_range);
1615 }
1616 _ => {
1617 for o in 0..out_dim {
1618 let (s1, s2) = row_dots(o);
1619 out1[o] = s1;
1620 out2[o] = s2;
1621 }
1622 }
1623 }
1624}
1625
1626pub(crate) struct SendMutT<T>(*mut T);
1629unsafe impl<T> Send for SendMutT<T> {}
1630unsafe impl<T> Sync for SendMutT<T> {}
1631impl<T> Clone for SendMutT<T> {
1632 fn clone(&self) -> Self {
1633 *self
1634 }
1635}
1636impl<T> Copy for SendMutT<T> {}
1637impl<T> SendMutT<T> {
1638 #[inline]
1639 pub(crate) fn new(p: *mut T) -> Self {
1640 Self(p)
1641 }
1642 #[inline]
1645 pub(crate) fn at(self, i: usize) -> *mut T {
1646 unsafe { self.0.add(i) }
1647 }
1648}
1649
1650#[derive(Clone, Copy)]
1651pub(crate) struct SendMut(*mut f32);
1652unsafe impl Send for SendMut {}
1653unsafe impl Sync for SendMut {}
1654
1655impl SendMut {
1656 #[inline]
1659 pub(crate) fn new(p: *mut f32) -> Self {
1660 Self(p)
1661 }
1662
1663 #[inline]
1666 pub(crate) fn at(self, i: usize) -> *mut f32 {
1667 unsafe { self.0.add(i) }
1668 }
1669}
1670
1671#[cfg(test)]
1672mod tests {
1673 #[test]
1677 fn float_activation_scope_follows_dispatch_and_is_restored() {
1678 let pool = Pool::with_spin(2, 0);
1679 let active = AtomicUsize::new(0);
1680 let inactive = AtomicUsize::new(0);
1681 crate::qtensor::float_activations_scope(|| {
1682 pool.run(&|_w, _n| {
1683 if crate::qtensor::float_activations_active() {
1684 active.fetch_add(1, Ordering::Relaxed);
1685 } else {
1686 inactive.fetch_add(1, Ordering::Relaxed);
1687 }
1688 });
1689 });
1690 assert_eq!(active.load(Ordering::Relaxed), 3);
1691 assert_eq!(inactive.load(Ordering::Relaxed), 0);
1692
1693 pool.run(&|_w, _n| {
1694 if crate::qtensor::float_activations_active() {
1695 active.fetch_add(1, Ordering::Relaxed);
1696 } else {
1697 inactive.fetch_add(1, Ordering::Relaxed);
1698 }
1699 });
1700 assert_eq!(active.load(Ordering::Relaxed), 3);
1701 assert_eq!(inactive.load(Ordering::Relaxed), 3);
1702 }
1703
1704 #[test]
1717 fn uninvited_straggler_never_runs_a_job_twice_or_wraps_the_barrier() {
1718 use std::sync::atomic::AtomicUsize;
1719 let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
1720 let workers = (cores * 4).clamp(8, 64);
1721 let pool = Arc::new(Pool::with_spin(workers, 1_000_000));
1722 let entries = Arc::new(AtomicUsize::new(0));
1723 let expected = Arc::new(AtomicUsize::new(0));
1724 let progress = Arc::new(AtomicUsize::new(0));
1725 let done = Arc::new(AtomicBool::new(false));
1726 let iters = 20_000usize;
1727 let p = pool.clone();
1728 let (en, ex, pr, dn) = (
1729 entries.clone(),
1730 expected.clone(),
1731 progress.clone(),
1732 done.clone(),
1733 );
1734 let driver = std::thread::spawn(move || {
1735 for i in 0..iters {
1736 pr.store(i, Ordering::Relaxed);
1737 let limit = 1 + i % 3;
1740 let f = |_w: usize, _n: usize| {
1741 en.fetch_add(1, Ordering::Relaxed);
1742 };
1743 p.run_limited(limit, &f);
1744 ex.fetch_add(limit.min(p.n_workers()) + 1, Ordering::Relaxed);
1745 if i % 97 == 0 {
1746 p.run(&f);
1747 ex.fetch_add(p.n_workers() + 1, Ordering::Relaxed);
1748 }
1749 }
1750 dn.store(true, Ordering::SeqCst);
1751 });
1752 let t0 = std::time::Instant::now();
1753 while !done.load(Ordering::SeqCst) {
1754 assert!(
1755 t0.elapsed() < std::time::Duration::from_secs(120),
1756 "pool dispatch loop did not finish: at iteration {} of {}, entries {} \
1757 expected {} (the difference is the job in flight), remaining {}",
1758 progress.load(Ordering::Relaxed),
1759 iters,
1760 entries.load(Ordering::Relaxed),
1761 expected.load(Ordering::Relaxed),
1762 pool.inner.remaining.0.load(Ordering::Relaxed)
1763 );
1764 std::thread::sleep(std::time::Duration::from_millis(20));
1765 }
1766 driver.join().unwrap();
1767 pool.run(&|_w, _n| {});
1770 assert_eq!(
1771 entries.load(Ordering::Relaxed),
1772 expected.load(Ordering::Relaxed),
1773 "some job ran a closure more or fewer times than its participants"
1774 );
1775 }
1776
1777 #[test]
1781 fn limited_dispatch_hands_tickets_only_to_invited_workers() {
1782 let pool = Pool::with_spin(6, 0);
1783 let hits: Vec<AtomicUsize> = (0..7).map(|_| AtomicUsize::new(0)).collect();
1784 let bad = AtomicUsize::new(0);
1785 let f = |w: usize, n: usize| match hits.get(w) {
1787 Some(h) if w < n && (n == 3 || n == 7) => {
1788 h.fetch_add(1, Ordering::Relaxed);
1789 }
1790 _ => {
1791 bad.fetch_add(1, Ordering::Relaxed);
1792 }
1793 };
1794 let tickets = |p: &Pool| -> Vec<usize> {
1795 p.inner
1796 .slots
1797 .iter()
1798 .map(|s| s.ticket.load(Ordering::Acquire))
1799 .collect()
1800 };
1801 pool.run_limited(2, &f);
1802 assert_eq!(tickets(&pool), [1, 1, 0, 0, 0, 0]);
1803 pool.run(&f);
1804 assert_eq!(tickets(&pool), [2, 2, 1, 1, 1, 1]);
1805 assert_eq!(
1806 bad.load(Ordering::Relaxed),
1807 0,
1808 "participant index or count off"
1809 );
1810 let hits: Vec<usize> = hits.iter().map(|h| h.load(Ordering::Relaxed)).collect();
1811 assert_eq!(hits, [2, 2, 2, 1, 1, 1, 1]);
1813 }
1814
1815 #[test]
1816 fn f32_four_row_matvec_matches_scalar_bits_and_preserves_tail() {
1817 let pool = super::Pool::new(3);
1818 for rows in [0, 1, 3, 4, 7, 255, 256, 259, 1024] {
1819 for cols in [0, 1, 3, 32, 65, 384] {
1820 let w: Vec<f32> = (0..rows * cols)
1821 .map(|i| (i as f32 * 0.173).sin() * [1e-3, 1.0, 1e3][i % 3])
1822 .collect();
1823 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.41).cos()).collect();
1824 let want: Vec<u32> = (0..rows)
1825 .map(|r| {
1826 let mut sum = 0.0f32;
1827 for j in 0..cols {
1828 sum += w[r * cols + j] * x[j];
1829 }
1830 sum.to_bits()
1831 })
1832 .collect();
1833 for workers in [None, Some(&pool)] {
1834 let mut out = vec![17.0f32; rows + 5];
1835 super::matvec_rows(workers, &w, &x, &mut out[..rows]);
1836 let bits: Vec<u32> = out[..rows].iter().map(|v| v.to_bits()).collect();
1837 assert_eq!(bits, want, "shape {rows}x{cols}");
1838 assert_eq!(&out[rows..], &[17.0; 5]);
1839 }
1840 let mut prefix = vec![0.0f32; rows / 2];
1842 super::matvec_rows(None, &w, &x, &mut prefix);
1843 assert_eq!(
1844 prefix.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
1845 want[..rows / 2]
1846 );
1847 }
1848 }
1849 }
1850
1851 #[test]
1852 #[cfg(target_os = "linux")]
1853 fn numa_cpulist_parses_ranges_and_singles() {
1854 assert_eq!(super::numa::parse_list("0-3,8,10-11\n"), vec![0, 1, 2, 3, 8, 10, 11]);
1855 assert_eq!(super::numa::parse_list(""), Vec::<usize>::new());
1856 }
1857
1858 #[test]
1859 #[cfg(any(target_os = "android", target_os = "linux"))]
1860 fn worker_tids_registered_before_new_returns() {
1861 use std::collections::HashSet;
1866 let p = super::Pool::new(3);
1867 let local: Vec<_> = p.inner.worker_tids.lock().unwrap().clone();
1868 let registered = p.inner.registered.load(Ordering::Acquire);
1869 let unique: HashSet<_> = local.iter().copied().collect();
1870 assert!(
1871 registered == 3
1872 && local.len() == 3
1873 && unique.len() == 3
1874 && local.iter().all(|&tid| tid > 0),
1875 "all worker tids must be privately registered before new returns \
1876 (registered {registered}, local {}, unique {})",
1877 local.len(),
1878 unique.len()
1879 );
1880 }
1881
1882 #[test]
1883 fn forced_threads_overrides_env_and_topology() {
1884 use std::sync::atomic::Ordering;
1885 super::FORCED_THREADS.store(3, Ordering::Relaxed);
1886 let pool = super::Pool::from_env().expect("forced 3 → pool");
1887 assert_eq!(pool.n_workers(), 3);
1888 super::FORCED_THREADS.store(1, Ordering::Relaxed);
1889 assert!(super::Pool::from_env().is_none(), "forced 1 → serial");
1890 super::FORCED_THREADS.store(0, Ordering::Relaxed);
1891 }
1892
1893 #[test]
1894 #[cfg(any(target_os = "android", target_os = "linux"))]
1895 fn concurrent_pool_constructors_complete_without_registration_race() {
1896 use std::sync::{Barrier, mpsc};
1903 use std::time::Duration;
1904
1905 for round in 0..16 {
1906 let start = Arc::new(Barrier::new(3));
1907 let (done_tx, done_rx) = mpsc::channel();
1908 let mut joins = Vec::new();
1909 for workers in [8usize, 1usize] {
1910 let start = start.clone();
1911 let done_tx = done_tx.clone();
1912 joins.push(std::thread::spawn(move || {
1913 start.wait();
1914 let pool = Pool::with_spin(workers, 0);
1915 done_tx.send(pool.n_workers()).unwrap();
1916 }));
1917 }
1918 drop(done_tx);
1919 start.wait();
1920 let mut sizes = Vec::with_capacity(2);
1921 for _ in 0..2 {
1922 sizes.push(
1923 done_rx
1924 .recv_timeout(Duration::from_secs(10))
1925 .unwrap_or_else(|_| panic!("pool constructor stalled in round {round}")),
1926 );
1927 }
1928 sizes.sort_unstable();
1929 assert_eq!(sizes, [1, 8]);
1930 for join in joins {
1931 join.join().unwrap();
1932 }
1933 }
1934 }
1935
1936 #[test]
1937 fn capacity_split_clock_bins_vs_microarch() {
1938 type P = super::Pool;
1939 assert_eq!(
1941 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 768, 768, 768, 768]),
1942 Some(8)
1943 );
1944 assert_eq!(
1946 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 350, 350, 350, 350]),
1947 Some(4)
1948 );
1949 assert_eq!(
1951 P::cores_from_capacities(&[1024, 800, 800, 800, 800, 300, 300, 300]),
1952 Some(5)
1953 );
1954 assert_eq!(P::cores_from_capacities(&[1024; 8]), None);
1956 assert_eq!(P::cores_from_capacities(&[]), None);
1957 }
1958
1959 use super::*;
1960
1961 #[test]
1962 fn parallel_matvec_equals_serial_bitexact() {
1963 let (out_dim, in_dim) = (512, 64);
1964 let w: Vec<f32> = (0..out_dim * in_dim)
1965 .map(|i| (i as f32 * 0.013).sin())
1966 .collect();
1967 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
1968
1969 let mut serial = vec![0.0f32; out_dim];
1970 matvec_rows(None, &w, &x, &mut serial);
1971
1972 let pool = Pool::new(4);
1973 let mut parallel = vec![0.0f32; out_dim];
1974 matvec_rows(Some(&pool), &w, &x, &mut parallel);
1975
1976 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
1977 }
1978
1979 #[test]
1980 fn fused_pair_equals_two_singles_bitexact() {
1981 let (out_dim, in_dim) = (300, 48);
1982 let w: Vec<f32> = (0..out_dim * in_dim)
1983 .map(|i| (i as f32 * 0.011).sin())
1984 .collect();
1985 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
1986 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
1987
1988 let mut a1 = vec![0.0f32; out_dim];
1989 let mut a2 = vec![0.0f32; out_dim];
1990 matvec_rows(None, &w, &x1, &mut a1);
1991 matvec_rows(None, &w, &x2, &mut a2);
1992
1993 for pool in [None, Some(Pool::new(3))] {
1994 let mut b1 = vec![0.0f32; out_dim];
1995 let mut b2 = vec![0.0f32; out_dim];
1996 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
1997 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
1998 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
1999 }
2000 }
2001
2002 #[test]
2003 fn pool_survives_many_runs() {
2004 let pool = Pool::new(3);
2005 let counter = AtomicUsize::new(0);
2006 for _ in 0..100 {
2007 pool.run(&|_, _| {
2008 counter.fetch_add(1, Ordering::Relaxed);
2009 });
2010 }
2011 assert_eq!(counter.load(Ordering::Relaxed), 400);
2013 }
2014
2015 #[test]
2016 fn pool_wakes_after_park() {
2017 let pool = Pool::with_spin(2, 0);
2020 let counter = AtomicUsize::new(0);
2021 for _ in 0..50 {
2022 pool.run(&|_, _| {
2023 counter.fetch_add(1, Ordering::Relaxed);
2024 });
2025 std::thread::sleep(std::time::Duration::from_micros(200));
2027 }
2028 assert_eq!(counter.load(Ordering::Relaxed), 150);
2029 }
2030
2031 #[test]
2032 fn worker_indices_are_distinct_and_cover_range() {
2033 let pool = Pool::new(3);
2034 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
2035 for _ in 0..20 {
2036 pool.run(&|widx, n| {
2037 assert_eq!(n, 4);
2038 hits[widx].fetch_add(1, Ordering::Relaxed);
2039 });
2040 }
2041 for (i, h) in hits.iter().enumerate() {
2042 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
2043 }
2044 }
2045}
2046
2047#[cfg(test)]
2048mod grain_tests {
2049 use super::grain_for;
2050
2051 #[test]
2052 fn a_short_job_still_reaches_every_worker() {
2053 assert_eq!(grain_for(24, 49), 1);
2056 assert_eq!(grain_for(4096, 49), 32);
2058 assert_eq!(grain_for(32768, 49), 83);
2059 assert_eq!(grain_for(0, 49), 1);
2061 assert_eq!(grain_for(7, 1), 7);
2062 assert!(grain_for(1, 49) >= 1);
2063 }
2064}