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 #[cfg(any(target_os = "android", target_os = "linux"))]
134 registered: AtomicUsize,
135 #[cfg(any(target_os = "android", target_os = "linux"))]
136 worker_tids: Mutex<Vec<i32>>,
137}
138
139static DISPATCHES: AtomicUsize = AtomicUsize::new(0);
142
143pub fn dispatch_count() -> usize {
145 DISPATCHES.load(Ordering::Relaxed)
146}
147
148pub struct Pool {
151 inner: Arc<Inner>,
152 threads: Vec<std::thread::Thread>,
154 joins: Vec<std::thread::JoinHandle<()>>,
155}
156
157fn spin_budget_from_env() -> usize {
158 std::env::var("CMF_POOL_SPIN")
159 .ok()
160 .and_then(|v| v.parse::<usize>().ok())
161 .unwrap_or(4000)
162}
163
164pub(crate) fn grain_for(rows: usize, workers: usize) -> usize {
174 if rows == 0 || workers <= 1 {
175 return rows.max(1);
176 }
177 let balanced = (rows / (workers * 8)).max(32);
178 balanced.min(rows.div_ceil(workers)).max(1)
180}
181
182impl Pool {
183 pub fn new(n_workers: usize) -> Self {
184 Self::with_spin(n_workers, spin_budget_from_env())
185 }
186
187 pub fn with_spin(n_workers: usize, spin_budget: usize) -> Self {
189 let cpus = std::thread::available_parallelism()
192 .map(|n| n.get())
193 .unwrap_or(1);
194 let inner = Arc::new(Inner {
195 epoch: AtomicUsize::new(0),
196 desc_data: AtomicUsize::new(0),
197 desc_vtable: AtomicUsize::new(0),
198 desc_n: AtomicUsize::new(0),
199 desc_dev: AtomicUsize::new(0),
200 desc_float_activations: AtomicBool::new(false),
201 remaining: Padded(AtomicUsize::new(0)),
202 slots: (0..n_workers)
203 .map(|_| Slot {
204 ticket: AtomicUsize::new(0),
205 parked: AtomicBool::new(false),
206 })
207 .collect(),
208 shutdown: AtomicBool::new(false),
209 spin_budget: AtomicUsize::new(spin_budget),
210 oversubscribed: n_workers + 1 > cpus,
211 #[cfg(any(target_os = "android", target_os = "linux"))]
212 registered: AtomicUsize::new(0),
213 #[cfg(any(target_os = "android", target_os = "linux"))]
214 worker_tids: Mutex::new(Vec::with_capacity(n_workers)),
215 });
216 let mut joins = Vec::with_capacity(n_workers);
217 for w in 0..n_workers {
218 let inner = inner.clone();
219 let h = std::thread::Builder::new()
220 .name(format!("cmf-pool-{w}"))
221 .spawn(move || {
222 #[cfg(any(target_os = "android", target_os = "linux"))]
223 {
224 let tid = unsafe { libc::gettid() } as i32;
225 if let Ok(mut tids) = inner.worker_tids.lock() {
226 tids.push(tid);
227 }
228 inner.registered.fetch_add(1, Ordering::Release);
229 }
230 worker_loop(&inner, w)
231 })
232 .expect("spawn pool worker");
233 joins.push(h);
234 }
235 #[cfg(any(target_os = "android", target_os = "linux"))]
242 while inner.registered.load(Ordering::Acquire) < n_workers {
243 std::thread::yield_now();
244 }
245 #[cfg(any(target_os = "android", target_os = "linux"))]
246 if let (Ok(mut global), Ok(local)) = (WORKER_TIDS.lock(), inner.worker_tids.lock()) {
247 *global = local.clone();
248 }
249 let threads = joins.iter().map(|h| h.thread().clone()).collect();
250 Self {
251 inner,
252 threads,
253 joins,
254 }
255 }
256
257 #[cfg(all(
263 target_arch = "aarch64",
264 any(target_os = "linux", target_os = "android")
265 ))]
266 fn big_cores() -> Option<usize> {
267 Self::cores_from_capacities(&core_capacities())
268 }
269
270 #[cfg_attr(
280 not(all(
281 target_arch = "aarch64",
282 any(target_os = "linux", target_os = "android")
283 )),
284 allow(dead_code)
285 )]
286 fn cores_from_capacities(caps: &[u64]) -> Option<usize> {
287 let max = *caps.iter().max()?;
288 let min = *caps.iter().min()?;
289 if caps.len() < 2 || max == min {
290 return None;
291 }
292 Some(caps.iter().filter(|&&c| c * 8 >= max * 5).count())
293 }
294
295 #[cfg(target_os = "macos")]
296 fn big_cores() -> Option<usize> {
297 if true {
305 return None;
306 }
307 #[allow(unreachable_code)]
308 unsafe extern "C" {
309 fn sysctlbyname(
310 name: *const std::ffi::c_char,
311 oldp: *mut std::ffi::c_void,
312 oldlenp: *mut usize,
313 newp: *mut std::ffi::c_void,
314 newlen: usize,
315 ) -> std::ffi::c_int;
316 }
317 unsafe {
318 let name = std::ffi::CString::new("hw.perflevel0.physicalcpu").ok()?;
319 let mut count: i32 = 0;
320 let mut size = std::mem::size_of::<i32>();
321 let ret = sysctlbyname(
322 name.as_ptr(),
323 &mut count as *mut i32 as *mut std::ffi::c_void,
324 &mut size,
325 std::ptr::null_mut(),
326 0,
327 );
328 if ret == 0 && count > 0 {
329 Some(count as usize)
330 } else {
331 None
332 }
333 }
334 }
335
336 #[cfg(not(any(
337 all(
338 target_arch = "aarch64",
339 any(target_os = "linux", target_os = "android")
340 ),
341 target_os = "macos"
342 )))]
343 fn big_cores() -> Option<usize> {
344 None
345 }
346
347 pub fn effective_threads() -> usize {
353 let forced = FORCED_THREADS.load(std::sync::atomic::Ordering::Relaxed);
354 if forced > 0 {
355 return forced;
356 }
357 match std::env::var("CMF_THREADS") {
358 Ok(v) => v.parse::<usize>().unwrap_or(0),
359 Err(_) => match Self::big_cores() {
360 Some(big) => big,
361 None => {
362 let avail = std::thread::available_parallelism()
372 .map(|n| n.get())
373 .unwrap_or(1);
374 avail.saturating_sub(1).min(32)
375 }
376 },
377 }
378 }
379
380 pub fn from_env() -> Option<Arc<Self>> {
383 let n = Self::effective_threads();
384 if n <= 1 {
385 None
386 } else {
387 Some(Arc::new(Self::new(n)))
388 }
389 }
390
391 pub fn n_workers(&self) -> usize {
393 self.threads.len()
394 }
395
396 pub fn bind_numa(&self, regions: &[&[u8]]) {
414 #[cfg(target_os = "linux")]
415 {
416 let Some((node, cpus)) = numa::choose(regions, self.threads.len() + 1) else {
417 return;
418 };
419 let mut applied = 0usize;
420 if let Ok(tids) = self.inner.worker_tids.lock() {
421 for &tid in tids.iter() {
422 if numa::set_affinity(tid, &cpus) {
423 applied += 1;
424 }
425 }
426 }
427 numa::publish(cpus.clone());
428 numa::adopt_caller();
429 tracing::info!(
430 "numa: pool bound to node {node} ({} cpus, {applied}/{} workers)",
431 cpus.len(),
432 self.threads.len()
433 );
434 if std::env::var("CMF_NUMA_TRACE").is_ok_and(|v| v != "0") {
435 eprintln!(
436 "numa: pool bound to node {node}: {} cpus, {applied}/{} workers",
437 cpus.len(),
438 self.threads.len()
439 );
440 }
441 }
442 #[cfg(not(target_os = "linux"))]
443 let _ = regions;
444 }
445
446 pub(crate) fn set_spin_budget(&self, spins: usize) {
450 self.inner.spin_budget.store(spins, Ordering::Relaxed);
451 }
452
453 fn dispatch(&self, f: &(dyn Fn(usize, usize) + Sync), nw: usize) {
460 let _tot = crate::cpuprof::time(crate::cpuprof::Slot::DispTotal);
461 let mut _setup = crate::cpuprof::time(crate::cpuprof::Slot::DispSetup);
462 let inner = &*self.inner;
463 let n = nw + 1;
464 let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
465 let raw: [usize; 2] = unsafe { std::mem::transmute(ptr) };
468 inner.desc_data.store(raw[0], Ordering::Relaxed);
469 inner.desc_vtable.store(raw[1], Ordering::Relaxed);
470 inner.desc_n.store(n, Ordering::Relaxed);
471 inner
472 .desc_dev
473 .store(crate::gpu::current_device(), Ordering::Relaxed);
474 inner.desc_float_activations.store(
475 crate::qtensor::float_activations_active(),
476 Ordering::Relaxed,
477 );
478 inner.remaining.0.store(nw, Ordering::Relaxed);
479 let e = inner.epoch.load(Ordering::Relaxed);
480 inner.epoch.store(e.wrapping_add(1), Ordering::Relaxed);
481 let slots = &inner.slots[..nw];
484 for slot in slots {
485 let t = slot.ticket.load(Ordering::Relaxed).wrapping_add(1);
486 slot.ticket.store(t, Ordering::Release);
487 }
488 std::sync::atomic::fence(Ordering::SeqCst);
493 for (slot, t) in slots.iter().zip(&self.threads) {
494 if slot.parked.load(Ordering::Relaxed) {
495 t.unpark();
496 }
497 }
498 drop(_setup);
499
500 let _work = crate::cpuprof::time(crate::cpuprof::Slot::DispWork);
503 f(nw, n);
504 drop(_work);
505
506 let _wait = crate::cpuprof::time(crate::cpuprof::Slot::DispWait);
510 let spin_limit = if inner.oversubscribed { 64 } else { 10_000 };
511 let mut spins = 0usize;
512 while inner.remaining.0.load(Ordering::Acquire) != 0 {
513 spins += 1;
514 if spins < spin_limit {
515 std::hint::spin_loop();
516 } else {
517 std::thread::yield_now();
518 }
519 }
520 drop(_wait);
521 drop(_tot);
522 }
523
524 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
535 let grain = grain_for(rows, self.threads.len() + 1);
536 let chunks = rows.div_ceil(grain.max(1));
537 let next = AtomicUsize::new(0);
538 self.run_limited(chunks, &|_w, _n| loop {
539 let start = next.fetch_add(grain, Ordering::Relaxed);
540 if start >= rows {
541 break;
542 }
543 f(start, (start + grain).min(rows));
544 });
545 }
546
547 fn run_limited(&self, max_workers: usize, f: &(dyn Fn(usize, usize) + Sync)) {
557 let nw = self.threads.len().min(max_workers);
558 if nw == self.threads.len() {
559 return self.run(f);
560 }
561 #[cfg(target_os = "linux")]
562 numa::adopt_caller();
563 DISPATCHES.fetch_add(1, Ordering::Relaxed);
564 self.dispatch(f, nw);
566 }
567
568 pub fn run_many(&self, parts: &[(usize, &(dyn Fn(usize, usize) + Sync))]) {
576 let total: usize = parts.iter().map(|p| p.0).sum();
577 if total == 0 {
578 return;
579 }
580 let grain = grain_for(total, self.threads.len() + 1);
581 let chunks = total.div_ceil(grain.max(1));
582 let next = AtomicUsize::new(0);
583 self.run_limited(chunks, &|_w, _n| loop {
584 let s = next.fetch_add(grain, Ordering::Relaxed);
585 if s >= total {
586 break;
587 }
588 let e = (s + grain).min(total);
589 let mut base = 0usize;
590 for &(rows, f) in parts {
591 let a = s.max(base);
592 let b = e.min(base + rows);
593 if a < b {
594 f(a - base, b - base);
595 }
596 base += rows;
597 if base >= e {
598 break;
599 }
600 }
601 });
602 }
603
604 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
608 #[cfg(target_os = "linux")]
609 numa::adopt_caller();
610 DISPATCHES.fetch_add(1, Ordering::Relaxed);
611 self.dispatch(f, self.threads.len());
612 }
613}
614
615impl Drop for Pool {
616 fn drop(&mut self) {
617 self.inner.shutdown.store(true, Ordering::SeqCst);
618 for t in &self.threads {
619 t.unpark();
620 }
621 for h in self.joins.drain(..) {
622 let _ = h.join();
623 }
624 }
625}
626
627#[cfg(target_os = "linux")]
629mod numa {
630 use std::sync::Mutex;
631 use std::sync::atomic::{AtomicUsize, Ordering};
632
633 static MASK: Mutex<Vec<usize>> = Mutex::new(Vec::new());
636 static EPOCH: AtomicUsize = AtomicUsize::new(0);
637 thread_local! {
638 static SEEN: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
639 }
640
641 pub(super) fn publish(cpus: Vec<usize>) {
642 if let Ok(mut m) = MASK.lock() {
643 *m = cpus;
644 }
645 EPOCH.fetch_add(1, Ordering::Release);
646 }
647
648 #[inline]
650 pub(super) fn adopt_caller() {
651 let e = EPOCH.load(Ordering::Acquire);
652 if e == 0 || SEEN.with(|c| c.get()) == e {
653 return;
654 }
655 SEEN.with(|c| c.set(e));
656 if let Ok(m) = MASK.lock() {
657 if !m.is_empty() {
658 set_affinity(0, &m);
659 }
660 }
661 }
662
663 pub(super) fn parse_list(s: &str) -> Vec<usize> {
664 let mut out = Vec::new();
665 for part in s.trim().split(',') {
666 let part = part.trim();
667 if part.is_empty() {
668 continue;
669 }
670 match part.split_once('-') {
671 Some((a, b)) => {
672 if let (Ok(a), Ok(b)) = (a.parse::<usize>(), b.parse::<usize>()) {
673 out.extend(a..=b);
674 }
675 }
676 None => {
677 if let Ok(a) = part.parse() {
678 out.push(a);
679 }
680 }
681 }
682 }
683 out
684 }
685
686 fn nodes() -> Vec<(usize, Vec<usize>)> {
687 let mut v = Vec::new();
688 let Ok(rd) = std::fs::read_dir("/sys/devices/system/node") else {
689 return v;
690 };
691 for e in rd.flatten() {
692 let name = e.file_name().to_string_lossy().to_string();
693 let Some(id) = name.strip_prefix("node").and_then(|x| x.parse::<usize>().ok()) else {
694 continue;
695 };
696 if let Ok(l) = std::fs::read_to_string(e.path().join("cpulist")) {
697 let cpus = parse_list(&l);
698 if !cpus.is_empty() {
699 v.push((id, cpus));
700 }
701 }
702 }
703 v.sort();
704 v
705 }
706
707 fn allowed() -> Vec<usize> {
708 unsafe {
710 let mut set: libc::cpu_set_t = std::mem::zeroed();
711 if libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut set) != 0 {
712 return Vec::new();
713 }
714 (0..libc::CPU_SETSIZE as usize)
715 .filter(|&c| libc::CPU_ISSET(c, &set))
716 .collect()
717 }
718 }
719
720 fn primary(cpu: usize) -> bool {
722 let p = format!("/sys/devices/system/cpu/cpu{cpu}/topology/thread_siblings_list");
723 match std::fs::read_to_string(p) {
724 Ok(l) => parse_list(&l).first().is_none_or(|&f| f == cpu),
725 Err(_) => true,
726 }
727 }
728
729 fn page_nodes(regions: &[&[u8]]) -> (usize, usize, Vec<usize>) {
739 const PAGE: usize = 4096;
740 let total: usize = regions.iter().map(|r| r.len() / PAGE).sum();
741 if total == 0 {
742 return (0, 0, Vec::new());
743 }
744 let stride = total.div_ceil(4096).max(1);
745 let mut pages: Vec<*mut libc::c_void> = Vec::new();
746 for r in regions {
747 let base = (r.as_ptr() as usize).div_ceil(PAGE) * PAGE;
748 let end = r.as_ptr() as usize + r.len();
749 let mut a = base;
750 while a + PAGE <= end {
751 pages.push(a as *mut libc::c_void);
752 a += PAGE * stride;
753 }
754 }
755 let mut incore = 0usize;
756 for &p in &pages {
757 let mut vec = 0u8;
758 let cached = unsafe { libc::mincore(p, PAGE, &mut vec) } == 0 && vec & 1 == 1;
760 if cached {
761 incore += 1;
762 unsafe { std::ptr::read_volatile(p as *const u8) };
764 }
765 }
766 let mut status = vec![-1i32; pages.len()];
767 let rc = unsafe {
770 libc::syscall(
771 libc::SYS_move_pages,
772 0,
773 pages.len() as libc::c_ulong,
774 pages.as_mut_ptr(),
775 std::ptr::null::<libc::c_int>(),
776 status.as_mut_ptr(),
777 0,
778 )
779 };
780 let mut by = Vec::new();
781 if rc == 0 {
782 for &st in &status {
783 if st >= 0 {
784 let n = st as usize;
785 if by.len() <= n {
786 by.resize(n + 1, 0);
787 }
788 by[n] += 1;
789 }
790 }
791 } else {
792 by = numa_maps_nodes(regions);
793 }
794 (pages.len(), incore, by)
795 }
796
797 fn numa_maps_nodes(regions: &[&[u8]]) -> Vec<usize> {
800 let (Ok(maps), Ok(nm)) = (
801 std::fs::read_to_string("/proc/self/maps"),
802 std::fs::read_to_string("/proc/self/numa_maps"),
803 ) else {
804 return Vec::new();
805 };
806 let spans: Vec<(usize, usize)> = regions
807 .iter()
808 .map(|r| (r.as_ptr() as usize, r.as_ptr() as usize + r.len()))
809 .collect();
810 let mut starts = std::collections::HashSet::new();
811 for line in maps.lines() {
812 let Some((range, _)) = line.split_once(' ') else {
813 continue;
814 };
815 let Some((a, b)) = range.split_once('-') else {
816 continue;
817 };
818 let (Ok(a), Ok(b)) = (usize::from_str_radix(a, 16), usize::from_str_radix(b, 16)) else {
819 continue;
820 };
821 if spans.iter().any(|&(s, e)| s < b && a < e) {
822 starts.insert(a);
823 }
824 }
825 let mut by = Vec::new();
826 for line in nm.lines() {
827 let mut it = line.split_whitespace();
828 let Some(a) = it.next().and_then(|a| usize::from_str_radix(a, 16).ok()) else {
829 continue;
830 };
831 if !starts.contains(&a) {
832 continue;
833 }
834 for f in it {
835 let Some((k, v)) = f.split_once('=') else {
836 continue;
837 };
838 let (Some(n), Ok(v)) = (
839 k.strip_prefix('N').and_then(|n| n.parse::<usize>().ok()),
840 v.parse::<usize>(),
841 ) else {
842 continue;
843 };
844 if by.len() <= n {
845 by.resize(n + 1, 0);
846 }
847 by[n] += v;
848 }
849 }
850 by
851 }
852
853 pub(super) fn choose(regions: &[&[u8]], threads: usize) -> Option<(usize, Vec<usize>)> {
855 let env = std::env::var("CMF_NUMA").ok();
856 if matches!(env.as_deref(), Some("0") | Some("off")) {
857 return None;
858 }
859 let trace = std::env::var("CMF_NUMA_TRACE").is_ok_and(|v| v != "0");
860 let nodes = nodes();
861 if nodes.len() < 2 {
862 if trace {
863 eprintln!("numa: {} node(s) visible — nothing to bind", nodes.len());
864 }
865 return None;
866 }
867 let forced = env
869 .as_deref()
870 .and_then(|v| v.strip_prefix("node:"))
871 .and_then(|v| v.parse::<usize>().ok());
872 let node = match forced {
873 Some(n) => n,
874 None => {
875 let (sampled, incore, by) = page_nodes(regions);
880 let resident: usize = by.iter().sum();
881 if trace {
882 eprintln!(
883 "numa: sampled {sampled} weight pages, {incore} cached, mapped by node {by:?}"
884 );
885 }
886 if sampled == 0 || incore * 2 < sampled || resident == 0 {
887 return None;
888 }
889 let (n, &cnt) = by.iter().enumerate().max_by_key(|(_, c)| **c)?;
890 if cnt * 10 < resident * 9 {
891 return None;
892 }
893 n
894 }
895 };
896 let cpus = &nodes.iter().find(|(id, _)| *id == node)?.1;
897 let allowed = allowed();
898 let usable: Vec<usize> = cpus.iter().copied().filter(|c| allowed.contains(c)).collect();
899 let prim: Vec<usize> = usable.iter().copied().filter(|&c| primary(c)).collect();
900 if prim.len() >= threads {
901 Some((node, prim))
902 } else if usable.len() >= threads {
903 Some((node, usable))
904 } else {
905 None
906 }
907 }
908
909 pub(super) fn set_affinity(tid: i32, cpus: &[usize]) -> bool {
911 unsafe {
913 let mut set: libc::cpu_set_t = std::mem::zeroed();
914 for &c in cpus {
915 if c < libc::CPU_SETSIZE as usize {
916 libc::CPU_SET(c, &mut set);
917 }
918 }
919 libc::sched_setaffinity(tid, std::mem::size_of::<libc::cpu_set_t>(), &set) == 0
920 }
921 }
922}
923
924#[cfg(any(
929 target_os = "android",
930 all(target_arch = "aarch64", target_os = "linux")
931))]
932fn core_capacities() -> Vec<u64> {
933 let read_all = |leaf: &str| -> Vec<u64> {
934 let mut vals = Vec::new();
935 for cpu in 0.. {
936 let path = format!("/sys/devices/system/cpu/cpu{cpu}/{leaf}");
937 match std::fs::read_to_string(&path) {
938 Ok(v) => match v.trim().parse() {
939 Ok(x) => vals.push(x),
940 Err(_) => break,
941 },
942 Err(_) => break,
943 }
944 }
945 vals
946 };
947 let caps = read_all("cpu_capacity");
948 if caps.len() >= 2 {
949 return caps;
950 }
951 read_all("cpufreq/cpuinfo_max_freq")
952}
953
954#[cfg(target_os = "android")]
955fn pin_thread_to_big_cores() {
956 use std::mem;
957 let caps = core_capacities();
958 let max = caps.iter().copied().max().unwrap_or(0);
959 let min = caps.iter().copied().min().unwrap_or(0);
960
961 if caps.len() < 2 || max == min {
963 return;
964 }
965
966 unsafe {
967 let mut set: libc::cpu_set_t = mem::zeroed();
968 for (i, &c) in caps.iter().enumerate() {
969 if c * 8 >= max * 5 {
970 libc::CPU_SET(i, &mut set);
971 }
972 }
973 libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &set);
974 }
975}
976
977fn worker_loop(inner: &Inner, idx: usize) {
978 #[cfg(target_os = "android")]
979 pin_thread_to_big_cores();
980 #[cfg(target_os = "macos")]
987 unsafe {
988 libc::pthread_set_qos_class_self_np(libc::qos_class_t::QOS_CLASS_USER_INITIATED, 0);
989 }
990
991 let slot = &inner.slots[idx];
992 let mut seen = 0usize;
997 let mut seen_epoch = 0usize;
998 loop {
999 let mut spins = 0usize;
1002 loop {
1003 let t = slot.ticket.load(Ordering::Acquire);
1004 if t != seen {
1005 seen = t;
1006 break;
1007 }
1008 if inner.shutdown.load(Ordering::Relaxed) {
1009 return;
1010 }
1011 if !inner.oversubscribed {
1012 let e = inner.epoch.load(Ordering::Relaxed);
1015 if e != seen_epoch {
1016 seen_epoch = e;
1017 spins = 0;
1018 }
1019 }
1020 if spins < inner.spin_budget.load(Ordering::Relaxed) {
1021 spins += 1;
1022 if inner.oversubscribed && spins.is_multiple_of(64) {
1023 std::thread::yield_now();
1024 } else {
1025 std::hint::spin_loop();
1026 }
1027 } else {
1028 slot.parked.store(true, Ordering::Relaxed);
1029 std::sync::atomic::fence(Ordering::SeqCst);
1034 if slot.ticket.load(Ordering::Relaxed) == seen
1035 && !inner.shutdown.load(Ordering::Relaxed)
1036 {
1037 std::thread::park();
1038 }
1039 slot.parked.store(false, Ordering::Relaxed);
1040 }
1041 }
1042 let data = inner.desc_data.load(Ordering::Relaxed);
1046 let vtable = inner.desc_vtable.load(Ordering::Relaxed);
1047 let n = inner.desc_n.load(Ordering::Relaxed);
1048 let dev = inner.desc_dev.load(Ordering::Relaxed);
1049 let float_activations = inner.desc_float_activations.load(Ordering::Relaxed);
1050 let task: *const (dyn Fn(usize, usize) + Sync + 'static) =
1054 unsafe { std::mem::transmute([data, vtable]) };
1055 let f = unsafe { &*task };
1056 crate::gpu::set_current_device(dev);
1057 let _float_activations =
1058 float_activations.then(crate::qtensor::enter_float_activations_scope);
1059 f(idx, n);
1060 inner.remaining.0.fetch_sub(1, Ordering::AcqRel);
1061 }
1062}
1063
1064pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
1067 let in_dim = x.len();
1068 let out_dim = out.len();
1069 debug_assert!(w.len() >= out_dim * in_dim);
1070
1071 let row_dot = |o: usize| -> f32 {
1072 let row = &w[o * in_dim..(o + 1) * in_dim];
1073 let mut sum = 0.0f32;
1074 for j in 0..in_dim {
1075 sum += row[j] * x[j];
1076 }
1077 sum
1078 };
1079
1080 let out_addr = SendMut(out.as_mut_ptr());
1081 let run_range = move |start: usize, end: usize| {
1082 let mut o = start;
1083 while end - o >= 4 {
1087 let base = o * in_dim;
1088 let w0 = &w[base..base + in_dim];
1089 let w1 = &w[base + in_dim..base + 2 * in_dim];
1090 let w2 = &w[base + 2 * in_dim..base + 3 * in_dim];
1091 let w3 = &w[base + 3 * in_dim..base + 4 * in_dim];
1092 let (mut a, mut b, mut c, mut d) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
1093 for j in 0..in_dim {
1094 let v = x[j];
1095 a += w0[j] * v;
1096 b += w1[j] * v;
1097 c += w2[j] * v;
1098 d += w3[j] * v;
1099 }
1100 unsafe {
1102 *out_addr.at(o) = a;
1103 *out_addr.at(o + 1) = b;
1104 *out_addr.at(o + 2) = c;
1105 *out_addr.at(o + 3) = d;
1106 }
1107 o += 4;
1108 }
1109 for o in o..end {
1110 unsafe { *out_addr.at(o) = row_dot(o) };
1111 }
1112 };
1113 match pool {
1114 Some(pool) if out_dim >= 256 => pool.run_rows(out_dim, &run_range),
1115 _ => run_range(0, out_dim),
1116 }
1117}
1118
1119pub fn matvec_rows2(
1125 pool: Option<&Pool>,
1126 w: &[f32],
1127 x1: &[f32],
1128 x2: &[f32],
1129 out1: &mut [f32],
1130 out2: &mut [f32],
1131) {
1132 let in_dim = x1.len();
1133 debug_assert_eq!(x2.len(), in_dim);
1134 let out_dim = out1.len();
1135 debug_assert_eq!(out2.len(), out_dim);
1136 debug_assert!(w.len() >= out_dim * in_dim);
1137
1138 let row_dots = |o: usize| -> (f32, f32) {
1139 let row = &w[o * in_dim..(o + 1) * in_dim];
1140 let (mut s1, mut s2) = (0.0f32, 0.0f32);
1141 for j in 0..in_dim {
1142 s1 += row[j] * x1[j];
1143 s2 += row[j] * x2[j];
1144 }
1145 (s1, s2)
1146 };
1147
1148 match pool {
1149 Some(pool) if out_dim >= 256 => {
1150 let o1 = SendMut(out1.as_mut_ptr());
1151 let o2 = SendMut(out2.as_mut_ptr());
1152 let run_range = move |start: usize, end: usize| {
1153 for o in start..end {
1154 let (s1, s2) = row_dots(o);
1155 unsafe {
1156 *o1.at(o) = s1;
1157 *o2.at(o) = s2;
1158 }
1159 }
1160 };
1161 pool.run_rows(out_dim, &run_range);
1162 }
1163 _ => {
1164 for o in 0..out_dim {
1165 let (s1, s2) = row_dots(o);
1166 out1[o] = s1;
1167 out2[o] = s2;
1168 }
1169 }
1170 }
1171}
1172
1173pub(crate) struct SendMutT<T>(*mut T);
1176unsafe impl<T> Send for SendMutT<T> {}
1177unsafe impl<T> Sync for SendMutT<T> {}
1178impl<T> Clone for SendMutT<T> {
1179 fn clone(&self) -> Self {
1180 *self
1181 }
1182}
1183impl<T> Copy for SendMutT<T> {}
1184impl<T> SendMutT<T> {
1185 #[inline]
1186 pub(crate) fn new(p: *mut T) -> Self {
1187 Self(p)
1188 }
1189 #[inline]
1192 pub(crate) fn at(self, i: usize) -> *mut T {
1193 unsafe { self.0.add(i) }
1194 }
1195}
1196
1197#[derive(Clone, Copy)]
1198pub(crate) struct SendMut(*mut f32);
1199unsafe impl Send for SendMut {}
1200unsafe impl Sync for SendMut {}
1201
1202impl SendMut {
1203 #[inline]
1206 pub(crate) fn new(p: *mut f32) -> Self {
1207 Self(p)
1208 }
1209
1210 #[inline]
1213 pub(crate) fn at(self, i: usize) -> *mut f32 {
1214 unsafe { self.0.add(i) }
1215 }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 #[test]
1224 fn float_activation_scope_follows_dispatch_and_is_restored() {
1225 let pool = Pool::with_spin(2, 0);
1226 let active = AtomicUsize::new(0);
1227 let inactive = AtomicUsize::new(0);
1228 crate::qtensor::float_activations_scope(|| {
1229 pool.run(&|_w, _n| {
1230 if crate::qtensor::float_activations_active() {
1231 active.fetch_add(1, Ordering::Relaxed);
1232 } else {
1233 inactive.fetch_add(1, Ordering::Relaxed);
1234 }
1235 });
1236 });
1237 assert_eq!(active.load(Ordering::Relaxed), 3);
1238 assert_eq!(inactive.load(Ordering::Relaxed), 0);
1239
1240 pool.run(&|_w, _n| {
1241 if crate::qtensor::float_activations_active() {
1242 active.fetch_add(1, Ordering::Relaxed);
1243 } else {
1244 inactive.fetch_add(1, Ordering::Relaxed);
1245 }
1246 });
1247 assert_eq!(active.load(Ordering::Relaxed), 3);
1248 assert_eq!(inactive.load(Ordering::Relaxed), 3);
1249 }
1250
1251 #[test]
1264 fn uninvited_straggler_never_runs_a_job_twice_or_wraps_the_barrier() {
1265 use std::sync::atomic::AtomicUsize;
1266 let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
1267 let workers = (cores * 4).clamp(8, 64);
1268 let pool = Arc::new(Pool::with_spin(workers, 1_000_000));
1269 let entries = Arc::new(AtomicUsize::new(0));
1270 let expected = Arc::new(AtomicUsize::new(0));
1271 let progress = Arc::new(AtomicUsize::new(0));
1272 let done = Arc::new(AtomicBool::new(false));
1273 let iters = 20_000usize;
1274 let p = pool.clone();
1275 let (en, ex, pr, dn) = (
1276 entries.clone(),
1277 expected.clone(),
1278 progress.clone(),
1279 done.clone(),
1280 );
1281 let driver = std::thread::spawn(move || {
1282 for i in 0..iters {
1283 pr.store(i, Ordering::Relaxed);
1284 let limit = 1 + i % 3;
1287 let f = |_w: usize, _n: usize| {
1288 en.fetch_add(1, Ordering::Relaxed);
1289 };
1290 p.run_limited(limit, &f);
1291 ex.fetch_add(limit.min(p.n_workers()) + 1, Ordering::Relaxed);
1292 if i % 97 == 0 {
1293 p.run(&f);
1294 ex.fetch_add(p.n_workers() + 1, Ordering::Relaxed);
1295 }
1296 }
1297 dn.store(true, Ordering::SeqCst);
1298 });
1299 let t0 = std::time::Instant::now();
1300 while !done.load(Ordering::SeqCst) {
1301 assert!(
1302 t0.elapsed() < std::time::Duration::from_secs(120),
1303 "pool dispatch loop did not finish: at iteration {} of {}, entries {} \
1304 expected {} (the difference is the job in flight), remaining {}",
1305 progress.load(Ordering::Relaxed),
1306 iters,
1307 entries.load(Ordering::Relaxed),
1308 expected.load(Ordering::Relaxed),
1309 pool.inner.remaining.0.load(Ordering::Relaxed)
1310 );
1311 std::thread::sleep(std::time::Duration::from_millis(20));
1312 }
1313 driver.join().unwrap();
1314 pool.run(&|_w, _n| {});
1317 assert_eq!(
1318 entries.load(Ordering::Relaxed),
1319 expected.load(Ordering::Relaxed),
1320 "some job ran a closure more or fewer times than its participants"
1321 );
1322 }
1323
1324 #[test]
1328 fn limited_dispatch_hands_tickets_only_to_invited_workers() {
1329 let pool = Pool::with_spin(6, 0);
1330 let hits: Vec<AtomicUsize> = (0..7).map(|_| AtomicUsize::new(0)).collect();
1331 let bad = AtomicUsize::new(0);
1332 let f = |w: usize, n: usize| match hits.get(w) {
1334 Some(h) if w < n && (n == 3 || n == 7) => {
1335 h.fetch_add(1, Ordering::Relaxed);
1336 }
1337 _ => {
1338 bad.fetch_add(1, Ordering::Relaxed);
1339 }
1340 };
1341 let tickets = |p: &Pool| -> Vec<usize> {
1342 p.inner
1343 .slots
1344 .iter()
1345 .map(|s| s.ticket.load(Ordering::Acquire))
1346 .collect()
1347 };
1348 pool.run_limited(2, &f);
1349 assert_eq!(tickets(&pool), [1, 1, 0, 0, 0, 0]);
1350 pool.run(&f);
1351 assert_eq!(tickets(&pool), [2, 2, 1, 1, 1, 1]);
1352 assert_eq!(
1353 bad.load(Ordering::Relaxed),
1354 0,
1355 "participant index or count off"
1356 );
1357 let hits: Vec<usize> = hits.iter().map(|h| h.load(Ordering::Relaxed)).collect();
1358 assert_eq!(hits, [2, 2, 2, 1, 1, 1, 1]);
1360 }
1361
1362 #[test]
1363 fn f32_four_row_matvec_matches_scalar_bits_and_preserves_tail() {
1364 let pool = super::Pool::new(3);
1365 for rows in [0, 1, 3, 4, 7, 255, 256, 259, 1024] {
1366 for cols in [0, 1, 3, 32, 65, 384] {
1367 let w: Vec<f32> = (0..rows * cols)
1368 .map(|i| (i as f32 * 0.173).sin() * [1e-3, 1.0, 1e3][i % 3])
1369 .collect();
1370 let x: Vec<f32> = (0..cols).map(|i| (i as f32 * 0.41).cos()).collect();
1371 let want: Vec<u32> = (0..rows)
1372 .map(|r| {
1373 let mut sum = 0.0f32;
1374 for j in 0..cols {
1375 sum += w[r * cols + j] * x[j];
1376 }
1377 sum.to_bits()
1378 })
1379 .collect();
1380 for workers in [None, Some(&pool)] {
1381 let mut out = vec![17.0f32; rows + 5];
1382 super::matvec_rows(workers, &w, &x, &mut out[..rows]);
1383 let bits: Vec<u32> = out[..rows].iter().map(|v| v.to_bits()).collect();
1384 assert_eq!(bits, want, "shape {rows}x{cols}");
1385 assert_eq!(&out[rows..], &[17.0; 5]);
1386 }
1387 let mut prefix = vec![0.0f32; rows / 2];
1389 super::matvec_rows(None, &w, &x, &mut prefix);
1390 assert_eq!(
1391 prefix.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
1392 want[..rows / 2]
1393 );
1394 }
1395 }
1396 }
1397
1398 #[test]
1399 #[cfg(target_os = "linux")]
1400 fn numa_cpulist_parses_ranges_and_singles() {
1401 assert_eq!(super::numa::parse_list("0-3,8,10-11\n"), vec![0, 1, 2, 3, 8, 10, 11]);
1402 assert_eq!(super::numa::parse_list(""), Vec::<usize>::new());
1403 }
1404
1405 #[test]
1406 #[cfg(any(target_os = "android", target_os = "linux"))]
1407 fn worker_tids_registered_before_new_returns() {
1408 use std::collections::HashSet;
1413 let p = super::Pool::new(3);
1414 let local: Vec<_> = p.inner.worker_tids.lock().unwrap().clone();
1415 let registered = p.inner.registered.load(Ordering::Acquire);
1416 let unique: HashSet<_> = local.iter().copied().collect();
1417 assert!(
1418 registered == 3
1419 && local.len() == 3
1420 && unique.len() == 3
1421 && local.iter().all(|&tid| tid > 0),
1422 "all worker tids must be privately registered before new returns \
1423 (registered {registered}, local {}, unique {})",
1424 local.len(),
1425 unique.len()
1426 );
1427 }
1428
1429 #[test]
1430 fn forced_threads_overrides_env_and_topology() {
1431 use std::sync::atomic::Ordering;
1432 super::FORCED_THREADS.store(3, Ordering::Relaxed);
1433 let pool = super::Pool::from_env().expect("forced 3 → pool");
1434 assert_eq!(pool.n_workers(), 3);
1435 super::FORCED_THREADS.store(1, Ordering::Relaxed);
1436 assert!(super::Pool::from_env().is_none(), "forced 1 → serial");
1437 super::FORCED_THREADS.store(0, Ordering::Relaxed);
1438 }
1439
1440 #[test]
1441 #[cfg(any(target_os = "android", target_os = "linux"))]
1442 fn concurrent_pool_constructors_complete_without_registration_race() {
1443 use std::sync::{Barrier, mpsc};
1450 use std::time::Duration;
1451
1452 for round in 0..16 {
1453 let start = Arc::new(Barrier::new(3));
1454 let (done_tx, done_rx) = mpsc::channel();
1455 let mut joins = Vec::new();
1456 for workers in [8usize, 1usize] {
1457 let start = start.clone();
1458 let done_tx = done_tx.clone();
1459 joins.push(std::thread::spawn(move || {
1460 start.wait();
1461 let pool = Pool::with_spin(workers, 0);
1462 done_tx.send(pool.n_workers()).unwrap();
1463 }));
1464 }
1465 drop(done_tx);
1466 start.wait();
1467 let mut sizes = Vec::with_capacity(2);
1468 for _ in 0..2 {
1469 sizes.push(
1470 done_rx
1471 .recv_timeout(Duration::from_secs(10))
1472 .unwrap_or_else(|_| panic!("pool constructor stalled in round {round}")),
1473 );
1474 }
1475 sizes.sort_unstable();
1476 assert_eq!(sizes, [1, 8]);
1477 for join in joins {
1478 join.join().unwrap();
1479 }
1480 }
1481 }
1482
1483 #[test]
1484 fn capacity_split_clock_bins_vs_microarch() {
1485 type P = super::Pool;
1486 assert_eq!(
1488 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 768, 768, 768, 768]),
1489 Some(8)
1490 );
1491 assert_eq!(
1493 P::cores_from_capacities(&[1024, 1024, 1024, 1024, 350, 350, 350, 350]),
1494 Some(4)
1495 );
1496 assert_eq!(
1498 P::cores_from_capacities(&[1024, 800, 800, 800, 800, 300, 300, 300]),
1499 Some(5)
1500 );
1501 assert_eq!(P::cores_from_capacities(&[1024; 8]), None);
1503 assert_eq!(P::cores_from_capacities(&[]), None);
1504 }
1505
1506 use super::*;
1507
1508 #[test]
1509 fn parallel_matvec_equals_serial_bitexact() {
1510 let (out_dim, in_dim) = (512, 64);
1511 let w: Vec<f32> = (0..out_dim * in_dim)
1512 .map(|i| (i as f32 * 0.013).sin())
1513 .collect();
1514 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
1515
1516 let mut serial = vec![0.0f32; out_dim];
1517 matvec_rows(None, &w, &x, &mut serial);
1518
1519 let pool = Pool::new(4);
1520 let mut parallel = vec![0.0f32; out_dim];
1521 matvec_rows(Some(&pool), &w, &x, &mut parallel);
1522
1523 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
1524 }
1525
1526 #[test]
1527 fn fused_pair_equals_two_singles_bitexact() {
1528 let (out_dim, in_dim) = (300, 48);
1529 let w: Vec<f32> = (0..out_dim * in_dim)
1530 .map(|i| (i as f32 * 0.011).sin())
1531 .collect();
1532 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
1533 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
1534
1535 let mut a1 = vec![0.0f32; out_dim];
1536 let mut a2 = vec![0.0f32; out_dim];
1537 matvec_rows(None, &w, &x1, &mut a1);
1538 matvec_rows(None, &w, &x2, &mut a2);
1539
1540 for pool in [None, Some(Pool::new(3))] {
1541 let mut b1 = vec![0.0f32; out_dim];
1542 let mut b2 = vec![0.0f32; out_dim];
1543 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
1544 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
1545 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
1546 }
1547 }
1548
1549 #[test]
1550 fn pool_survives_many_runs() {
1551 let pool = Pool::new(3);
1552 let counter = AtomicUsize::new(0);
1553 for _ in 0..100 {
1554 pool.run(&|_, _| {
1555 counter.fetch_add(1, Ordering::Relaxed);
1556 });
1557 }
1558 assert_eq!(counter.load(Ordering::Relaxed), 400);
1560 }
1561
1562 #[test]
1563 fn pool_wakes_after_park() {
1564 let pool = Pool::with_spin(2, 0);
1567 let counter = AtomicUsize::new(0);
1568 for _ in 0..50 {
1569 pool.run(&|_, _| {
1570 counter.fetch_add(1, Ordering::Relaxed);
1571 });
1572 std::thread::sleep(std::time::Duration::from_micros(200));
1574 }
1575 assert_eq!(counter.load(Ordering::Relaxed), 150);
1576 }
1577
1578 #[test]
1579 fn worker_indices_are_distinct_and_cover_range() {
1580 let pool = Pool::new(3);
1581 let hits: Vec<AtomicUsize> = (0..4).map(|_| AtomicUsize::new(0)).collect();
1582 for _ in 0..20 {
1583 pool.run(&|widx, n| {
1584 assert_eq!(n, 4);
1585 hits[widx].fetch_add(1, Ordering::Relaxed);
1586 });
1587 }
1588 for (i, h) in hits.iter().enumerate() {
1589 assert_eq!(h.load(Ordering::Relaxed), 20, "participant {i} missed runs");
1590 }
1591 }
1592}
1593
1594#[cfg(test)]
1595mod grain_tests {
1596 use super::grain_for;
1597
1598 #[test]
1599 fn a_short_job_still_reaches_every_worker() {
1600 assert_eq!(grain_for(24, 49), 1);
1603 assert_eq!(grain_for(4096, 49), 32);
1605 assert_eq!(grain_for(32768, 49), 83);
1606 assert_eq!(grain_for(0, 49), 1);
1608 assert_eq!(grain_for(7, 1), 7);
1609 assert!(grain_for(1, 49) >= 1);
1610 }
1611}