1#[cfg(not(target_arch = "wasm32"))]
13use rayon::prelude::*;
14#[cfg(not(target_arch = "wasm32"))]
15use std::sync::Once;
16
17#[cfg(not(target_arch = "wasm32"))]
18static POOL_INIT: Once = Once::new();
19
20#[cfg(not(target_arch = "wasm32"))]
21fn ensure_pool() {
22 POOL_INIT.call_once(|| {
23 let cfg = crate::config::RuntimeConfig::global();
24 let n = cfg.pool_workers.max(1);
25 let _ = rayon::ThreadPoolBuilder::new()
26 .num_threads(n)
27 .thread_name(|i| format!("rlx-rayon-{i}"))
28 .build_global();
29 crate::blas::limit_inner_threads();
32 });
33}
34
35#[cfg(not(target_arch = "wasm32"))]
40pub fn num_threads() -> usize {
41 ensure_pool();
42 rayon::current_num_threads()
43}
44
45#[cfg(target_arch = "wasm32")]
46pub fn num_threads() -> usize {
47 1
48}
49
50#[inline]
59pub fn par_range<F: Fn(usize) + Sync + Send>(n: usize, f: F) {
60 #[cfg(target_arch = "wasm32")]
61 {
62 (0..n).for_each(f);
63 }
64 #[cfg(not(target_arch = "wasm32"))]
65 {
66 (0..n).into_par_iter().for_each(f);
67 }
68}
69
70pub const ELEMENTWISE_PAR_FLOOR: usize = 4096;
75
76#[inline]
80pub fn should_parallelize(work: usize) -> bool {
81 num_threads() > 1 && work >= ELEMENTWISE_PAR_FLOOR * 2
82}
83
84#[inline]
87pub fn chunk_floor(total: usize) -> usize {
88 (total / num_threads().max(1)).max(ELEMENTWISE_PAR_FLOOR)
89}
90
91#[inline]
94pub fn outer_chunk(units: usize) -> usize {
95 (units / num_threads().max(1)).max(1)
96}
97
98#[inline]
99pub fn par_for<F: Fn(usize, usize) + Sync>(total: usize, min_per_thread: usize, f: &F) {
100 if total == 0 {
101 return;
102 }
103 #[cfg(target_arch = "wasm32")]
106 {
107 let _ = min_per_thread;
108 f(0, total);
109 }
110 #[cfg(not(target_arch = "wasm32"))]
111 {
112 ensure_pool();
113 let grain = min_per_thread.max(1);
114 let n_threads = (total / grain).max(1).min(num_threads());
115 if n_threads <= 1 {
116 f(0, total);
117 return;
118 }
119 crate::blas::ensure_blas_threads(1);
122 let chunk = total.div_ceil(n_threads);
123 (0..n_threads).into_par_iter().for_each(|t| {
124 let off = t * chunk;
125 if off < total {
126 f(off, (off + chunk).min(total) - off);
127 }
128 });
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::sync::atomic::{AtomicU64, Ordering};
136
137 #[test]
138 fn par_for_sums_correctly() {
139 let data = vec![1.0f32; 10_000];
140 let total = AtomicU64::new(0);
141
142 par_for(data.len(), 100, &|off, cnt| {
143 let partial: f32 = data[off..off + cnt].iter().sum();
144 total.fetch_add(partial.to_bits() as u64, Ordering::Relaxed);
145 });
146
147 assert!(total.load(Ordering::Relaxed) > 0);
148 }
149
150 #[test]
151 fn par_for_small_is_sequential() {
152 let sum = std::sync::atomic::AtomicUsize::new(0);
153 par_for(10, 100, &|off, cnt| {
154 sum.fetch_add(cnt, Ordering::Relaxed);
155 assert_eq!(off + cnt, 10);
156 });
157 assert_eq!(sum.load(Ordering::Relaxed), 10);
158 }
159
160 #[test]
161 fn par_for_exact_sum_many_dispatches() {
162 for &n in &[256usize, 1024, 4097] {
163 let sum = std::sync::atomic::AtomicUsize::new(0);
164 par_for(n, 256, &|off, cnt| {
165 sum.fetch_add(cnt, Ordering::Relaxed);
166 assert!(off + cnt <= n);
167 });
168 assert_eq!(sum.load(Ordering::Relaxed), n);
169 }
170 }
171
172 #[test]
173 fn par_for_concurrent_callers_isolated() {
174 std::thread::scope(|s| {
175 for t in 0..4 {
176 s.spawn(move || {
177 let n = 4096 + t * 17;
178 let sum = std::sync::atomic::AtomicUsize::new(0);
179 par_for(n, 128, &|off, cnt| {
180 sum.fetch_add(cnt, Ordering::Relaxed);
181 assert!(off + cnt <= n);
182 });
183 assert_eq!(sum.load(Ordering::Relaxed), n);
184 });
185 }
186 });
187 }
188}