1use rayon::prelude::*;
31use std::sync::OnceLock;
32
33#[cfg(target_os = "macos")]
35mod qos {
36 pub const QOS_CLASS_USER_INTERACTIVE: u32 = 0x21;
37 pub const QOS_CLASS_USER_INITIATED: u32 = 0x19;
38 pub const QOS_CLASS_DEFAULT: u32 = 0x15;
39 pub const QOS_CLASS_UTILITY: u32 = 0x11;
40 pub const QOS_CLASS_BACKGROUND: u32 = 0x09;
41
42 extern "C" {
43 pub fn pthread_set_qos_class_self_np(qos: u32, relative_priority: i32) -> i32;
44 pub fn qos_class_self() -> u32;
45 }
46
47 pub fn name(class: u32) -> &'static str {
48 match class {
49 QOS_CLASS_USER_INTERACTIVE => "user-interactive",
50 QOS_CLASS_USER_INITIATED => "user-initiated",
51 QOS_CLASS_DEFAULT => "default",
52 QOS_CLASS_UTILITY => "utility",
53 QOS_CLASS_BACKGROUND => "background",
54 _ => "unspecified",
55 }
56 }
57}
58
59pub fn current_qos_name() -> Option<&'static str> {
62 #[cfg(target_os = "macos")]
63 {
64 Some(qos::name(unsafe { qos::qos_class_self() }))
65 }
66 #[cfg(not(target_os = "macos"))]
67 {
68 None
69 }
70}
71
72pub fn perf_core_count() -> usize {
77 #[cfg(target_os = "macos")]
78 {
79 if let Some(n) = sysctl_usize("hw.perflevel0.physicalcpu") {
80 if n > 0 {
81 return n;
82 }
83 }
84 }
85 std::thread::available_parallelism()
86 .map(|n| n.get())
87 .unwrap_or(1)
88}
89
90#[cfg(target_os = "macos")]
91fn sysctl_usize(name: &str) -> Option<usize> {
92 use std::ffi::CString;
93 extern "C" {
94 fn sysctlbyname(
95 name: *const std::os::raw::c_char,
96 oldp: *mut std::ffi::c_void,
97 oldlenp: *mut usize,
98 newp: *mut std::ffi::c_void,
99 newlen: usize,
100 ) -> std::os::raw::c_int;
101 }
102 let key = CString::new(name).ok()?;
103 let mut out: i32 = 0;
104 let mut len = std::mem::size_of::<i32>();
105 let rc = unsafe {
108 sysctlbyname(
109 key.as_ptr(),
110 &mut out as *mut i32 as *mut std::ffi::c_void,
111 &mut len,
112 std::ptr::null_mut(),
113 0,
114 )
115 };
116 if rc == 0 && out > 0 {
117 Some(out as usize)
118 } else {
119 None
120 }
121}
122
123pub fn resolve_cpu_threads() -> usize {
126 for key in ["FERROX_CPU_THREADS", "RAYON_NUM_THREADS"] {
127 if let Ok(v) = std::env::var(key) {
128 if let Ok(n) = v.trim().parse::<usize>() {
129 if n > 0 {
130 return n;
131 }
132 }
133 }
134 }
135 perf_core_count()
136}
137
138pub fn resolve_gemv_threads() -> usize {
140 if let Ok(v) = std::env::var("FERROX_GEMV_THREADS") {
141 if let Ok(n) = v.trim().parse::<usize>() {
142 if n > 0 {
143 return n;
144 }
145 }
146 }
147 resolve_cpu_threads()
148}
149
150static GEMV_POOL: OnceLock<Option<rayon::ThreadPool>> = OnceLock::new();
151
152fn gemv_pool_qos_start_handler(idx: usize) {
153 #[cfg(target_os = "macos")]
154 {
155 let log = std::env::var_os("FERROX_QOS_LOG").is_some();
156 let before = unsafe { qos::qos_class_self() };
157 let rc = unsafe { qos::pthread_set_qos_class_self_np(qos::QOS_CLASS_USER_INTERACTIVE, 0) };
158 if log {
159 eprintln!(
160 "ferrox: gemv worker {idx} qos {} -> {} (rc={rc})",
161 qos::name(before),
162 qos::name(unsafe { qos::qos_class_self() }),
163 );
164 }
165 }
166 #[cfg(not(target_os = "macos"))]
167 {
168 let _ = idx;
169 }
170}
171
172fn gemv_pool() -> Option<&'static rayon::ThreadPool> {
173 GEMV_POOL
174 .get_or_init(|| {
175 rayon::ThreadPoolBuilder::new()
176 .num_threads(resolve_gemv_threads())
177 .thread_name(|i| format!("ferrox-gemv-{i}"))
178 .start_handler(gemv_pool_qos_start_handler)
179 .build()
180 .ok()
181 })
182 .as_ref()
183}
184
185pub fn init_gemv_pool() {
187 let _ = gemv_pool();
188}
189
190pub fn gemv_num_threads() -> usize {
192 match gemv_pool() {
193 Some(pool) => pool.current_num_threads(),
194 None => 1,
195 }
196}
197
198pub fn should_parallelize(n_rows: usize, n_cols: usize) -> bool {
201 n_rows > 1 && n_rows.saturating_mul(n_cols) >= 256_000
202}
203
204pub fn for_each_row<F>(output: &mut [f32], n_rows: usize, n_cols: usize, row_fn: F)
207where
208 F: Fn(usize, &mut f32) + Send + Sync,
209{
210 let n = n_rows.min(output.len());
211 if !should_parallelize(n, n_cols) {
212 for (row, out) in output.iter_mut().enumerate().take(n) {
213 row_fn(row, out);
214 }
215 return;
216 }
217 let use_dedicated = matches!(
220 std::env::var("FERROX_GEMV_DEDICATED").ok().as_deref(),
221 Some("1") | Some("true") | Some("on")
222 );
223 let rows = &mut output[..n];
224 if use_dedicated {
225 if let Some(pool) = gemv_pool() {
226 if pool.current_num_threads() > 1 {
227 pool.install(move || {
228 rows.par_iter_mut()
229 .enumerate()
230 .for_each(|(row, out)| row_fn(row, out));
231 });
232 return;
233 }
234 }
235 }
236 rows.par_iter_mut()
237 .enumerate()
238 .for_each(|(row, out)| row_fn(row, out));
239}
240
241pub fn for_each_chunk_init<S, I, F>(
243 output: &mut [f32],
244 chunk_len: usize,
245 work_per_chunk: usize,
246 init: I,
247 f: F,
248) where
249 I: Fn() -> S + Send + Sync,
250 S: Send,
251 F: Fn(&mut S, usize, &mut [f32]) + Send + Sync,
252{
253 if chunk_len == 0 {
254 return;
255 }
256 let n_chunks = output.len() / chunk_len;
257 if !should_parallelize(n_chunks, work_per_chunk) {
258 let mut state = init();
259 for (i, chunk) in output[..n_chunks * chunk_len]
260 .chunks_mut(chunk_len)
261 .enumerate()
262 {
263 f(&mut state, i, chunk);
264 }
265 return;
266 }
267 let use_dedicated = matches!(
268 std::env::var("FERROX_GEMV_DEDICATED").ok().as_deref(),
269 Some("1") | Some("true") | Some("on")
270 );
271 let chunks = &mut output[..n_chunks * chunk_len];
272 let init = &init;
273 let f = &f;
274 if use_dedicated {
275 if let Some(pool) = gemv_pool() {
276 if pool.current_num_threads() > 1 {
277 pool.install(move || {
278 chunks
279 .par_chunks_mut(chunk_len)
280 .enumerate()
281 .for_each_init(init, |state, (i, c)| f(state, i, c));
282 });
283 return;
284 }
285 }
286 }
287 chunks
288 .par_chunks_mut(chunk_len)
289 .enumerate()
290 .for_each_init(init, |state, (i, c)| f(state, i, c));
291}
292
293pub fn init_cpu_pool() -> Option<usize> {
308 let threads = resolve_cpu_threads();
313 let log = std::env::var_os("FERROX_QOS_LOG").is_some();
314 let built = rayon::ThreadPoolBuilder::new()
315 .num_threads(threads)
316 .start_handler(move |idx| {
317 #[cfg(target_os = "macos")]
318 {
319 let before = unsafe { qos::qos_class_self() };
320 let rc = unsafe {
322 qos::pthread_set_qos_class_self_np(qos::QOS_CLASS_USER_INTERACTIVE, 0)
323 };
324 if log {
325 eprintln!(
326 "ferrox: rayon worker {idx} qos {} -> {} (rc={rc})",
327 qos::name(before),
328 qos::name(unsafe { qos::qos_class_self() }),
329 );
330 }
331 }
332 #[cfg(not(target_os = "macos"))]
333 {
334 let _ = (idx, log);
335 }
336 })
337 .build_global()
338 .is_ok();
339 if built {
340 Some(threads)
341 } else {
342 None
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn perf_core_count_is_at_least_one_and_no_more_than_logical_cores() {
352 let logical = std::thread::available_parallelism()
353 .map(|n| n.get())
354 .unwrap_or(1);
355 let perf = perf_core_count();
356 assert!(perf >= 1, "perf core count must be positive, got {perf}");
357 assert!(
358 perf <= logical,
359 "perf cores ({perf}) cannot exceed logical cores ({logical})"
360 );
361 }
362
363 #[test]
364 fn resolved_thread_count_falls_back_to_perf_cores_without_env_overrides() {
365 if std::env::var_os("FERROX_CPU_THREADS").is_none()
368 && std::env::var_os("RAYON_NUM_THREADS").is_none()
369 {
370 assert_eq!(resolve_cpu_threads(), perf_core_count());
371 }
372 }
373
374 #[test]
375 fn current_qos_name_is_reported_on_macos_and_absent_elsewhere() {
376 let qos = current_qos_name();
377 #[cfg(target_os = "macos")]
378 assert!(qos.is_some(), "macOS must report a QoS class");
379 #[cfg(not(target_os = "macos"))]
380 assert!(qos.is_none(), "QoS is a macOS-only concept");
381 }
382
383 #[test]
384 fn for_each_row_parallel_matches_serial() {
385 let n = 4097usize;
386 let f = |row: usize| ((row % 97) as f32) * 0.25 - 3.0;
387
388 let mut par = vec![0.0f32; n];
389 for_each_row(&mut par, n, 4096, |row, slot| *slot = f(row));
390
391 let mut serial = vec![0.0f32; n];
392 for (row, slot) in serial.iter_mut().enumerate() {
393 *slot = f(row);
394 }
395 assert_eq!(par, serial);
396 }
397}