1use std::cell::RefCell;
2#[cfg(feature = "multithread-mm")]
3use std::sync::atomic::{AtomicUsize, Ordering};
4#[allow(unused_imports)]
5use std::sync::{Arc, Mutex};
6
7#[cfg(feature = "multithread-mm")]
8use rayon::{ThreadPool, ThreadPoolBuilder};
9
10use tract_data::internal::num_integer::Integer;
11use tract_data::internal::vector_size;
12use tract_data::internal::{Tensor, TensorView, TractResult, ensure};
13
14use crate::BinFn;
15
16#[derive(Debug, Clone, Default)]
17pub enum Executor {
18 #[default]
19 SingleThread,
20 #[cfg(feature = "multithread-mm")]
21 MultiThread(Arc<ThreadPool>),
22 #[cfg(feature = "multithread-mm")]
31 RayonGlobal,
32}
33
34impl Executor {
35 #[cfg(feature = "multithread-mm")]
36 pub fn multithread(n: usize) -> Executor {
37 Executor::multithread_with_name(n, "tract-default")
38 }
39
40 #[cfg(feature = "multithread-mm")]
47 pub fn multithread_with_name(n: usize, name: &str) -> Executor {
48 let name = name.to_string();
49 match ThreadPoolBuilder::new()
50 .thread_name(move |n| format!("{name}-{n}"))
51 .num_threads(n)
52 .build()
53 {
54 Ok(pool) => Executor::MultiThread(Arc::new(pool)),
55 Err(_) if cfg!(target_family = "wasm") => Executor::RayonGlobal,
56 Err(e) => panic!("Failed to build a {n}-thread pool: {e}"),
57 }
58 }
59}
60
61static DEFAULT_EXECUTOR: Mutex<Executor> = Mutex::new(Executor::SingleThread);
62
63thread_local! {
64 static TLS_EXECUTOR_OVERRIDE: RefCell<Option<Executor>> = Default::default();
65}
66
67pub fn current_tract_executor() -> Executor {
68 if let Some(over_ride) = TLS_EXECUTOR_OVERRIDE.with_borrow(|tls| tls.clone()) {
69 over_ride
70 } else {
71 DEFAULT_EXECUTOR.lock().unwrap().clone()
72 }
73}
74
75pub fn set_default_executor(executor: Executor) {
76 *DEFAULT_EXECUTOR.lock().unwrap() = executor;
77}
78
79pub fn multithread_tract_scope<R, F: FnOnce() -> R>(pool: Executor, f: F) -> R {
80 let previous = TLS_EXECUTOR_OVERRIDE.replace(Some(pool));
81 let result = f();
82 TLS_EXECUTOR_OVERRIDE.set(previous);
83 result
84}
85
86#[cfg(feature = "multithread-mm")]
95static THREADING_PANEL_THRESHOLD: AtomicUsize = AtomicUsize::new(64);
96
97#[cfg(feature = "multithread-mm")]
99pub fn current_threading_panel_threshold() -> usize {
100 THREADING_PANEL_THRESHOLD.load(Ordering::Relaxed)
101}
102
103#[cfg(feature = "multithread-mm")]
106pub fn set_threading_panel_threshold(panels: usize) {
107 THREADING_PANEL_THRESHOLD.store(panels, Ordering::Relaxed);
108}
109
110#[cfg(feature = "multithread-mm")]
120static THREADING_MAC_THRESHOLD: AtomicUsize =
121 AtomicUsize::new(if cfg!(target_family = "wasm") { usize::MAX } else { 1 << 21 });
122
123#[cfg(feature = "multithread-mm")]
125pub fn current_threading_mac_threshold() -> usize {
126 THREADING_MAC_THRESHOLD.load(Ordering::Relaxed)
127}
128
129#[cfg(feature = "multithread-mm")]
132pub fn set_threading_mac_threshold(macs: usize) {
133 THREADING_MAC_THRESHOLD.store(macs, Ordering::Relaxed);
134}
135
136#[cfg(feature = "multithread-mm")]
143static THREADING_ELEMENT_THRESHOLD: AtomicUsize = AtomicUsize::new(32768);
144
145#[cfg(feature = "multithread-mm")]
147pub fn current_threading_element_threshold() -> usize {
148 THREADING_ELEMENT_THRESHOLD.load(Ordering::Relaxed)
149}
150
151#[cfg(feature = "multithread-mm")]
154pub fn set_threading_element_threshold(elements: usize) {
155 THREADING_ELEMENT_THRESHOLD.store(elements, Ordering::Relaxed);
156}
157
158pub fn par_chunks_mut<T: Send>(
180 out: &mut [T],
181 row_len: usize,
182 total_elems: usize,
183 f: impl Fn(usize, &mut [T]) -> TractResult<()> + Sync + Send,
184) -> TractResult<()> {
185 #[cfg(feature = "multithread-mm")]
186 {
187 use rayon::prelude::*;
188 debug_assert!(row_len >= 1 && out.len() % row_len == 0);
189 let n_rows = out.len() / row_len;
190 if n_rows < 2 || total_elems < current_threading_element_threshold() {
191 return f(0, out);
192 }
193 let run = |out: &mut [T]| -> TractResult<()> {
194 let n_chunks = (4 * rayon::current_num_threads()).min(n_rows);
195 let chunk_rows = n_rows.div_ceil(n_chunks);
196 out.par_chunks_mut(chunk_rows * row_len)
197 .enumerate()
198 .try_for_each(|(i, chunk)| f(i * chunk_rows, chunk))
199 };
200 match current_tract_executor() {
201 Executor::MultiThread(pool) if pool.current_num_threads() > 1 => {
202 pool.install(|| run(out))
203 }
204 Executor::RayonGlobal => run(out),
205 _ => f(0, out),
207 }
208 }
209 #[cfg(not(feature = "multithread-mm"))]
210 {
211 let _ = (row_len, total_elems);
212 f(0, out)
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum BShare {
219 Lockstep,
222 PerBlock,
225}
226
227pub fn par_bin(
251 eval_fn: &BinFn,
252 a: &mut Tensor,
253 b: &Tensor,
254 period: usize,
255 share: BShare,
256) -> TractResult<()> {
257 if a.len() == 0 {
258 return Ok(());
259 }
260 ensure!(
261 a.len().is_multiple_of(period),
262 "par_bin: period {period} does not divide a.len() {}",
263 a.len()
264 );
265 let n_blocks = a.len() / period;
266 match share {
267 BShare::Lockstep => ensure!(
268 b.len() == period,
269 "par_bin: Lockstep wants b.len() {} == period {period}",
270 b.len()
271 ),
272 BShare::PerBlock => ensure!(
273 b.len() == n_blocks,
274 "par_bin: PerBlock wants b.len() {} == {n_blocks} blocks",
275 b.len()
276 ),
277 }
278
279 let a_item = a.datum_type().size_of() as isize;
280 let b_item = b.datum_type().size_of() as isize;
281 let a = &*a;
282 let call = |block: usize, offset: usize, len: usize| -> TractResult<()> {
287 static STRIDES: [isize; 1] = [1];
288 let a_shape = [len];
289 let (b_offset, b_shape) = match share {
290 BShare::Lockstep => (offset as isize * b_item, [len]),
291 BShare::PerBlock => (block as isize * b_item, [1]),
292 };
293 let a_offset = (block * period + offset) as isize * a_item;
294 unsafe {
300 let mut a_chunk = TensorView::from_bytes(a, a_offset, &a_shape, &STRIDES);
301 let b_chunk = TensorView::from_bytes(b, b_offset, &b_shape, &STRIDES);
302 eval_fn(&mut a_chunk, &b_chunk)
303 }
304 };
305
306 #[cfg(feature = "multithread-mm")]
307 {
308 use rayon::prelude::*;
309 if a.len() >= current_threading_element_threshold() {
312 let executor = current_tract_executor();
313 let nth = match &executor {
314 Executor::MultiThread(pool) => pool.current_num_threads(),
315 Executor::RayonGlobal => rayon::current_num_threads(),
316 Executor::SingleThread => 1,
317 };
318 if nth > 1 {
319 let per_block = (4 * nth).div_ceil(n_blocks).max(1);
320 let chunk = period.div_ceil(per_block).next_multiple_of(vector_size()).min(period);
321 let per_block = period.div_ceil(chunk);
322 if n_blocks * per_block > 1 {
323 let run = || {
324 (0..n_blocks * per_block).into_par_iter().try_for_each(|i| {
325 let offset = (i % per_block) * chunk;
326 call(i / per_block, offset, chunk.min(period - offset))
327 })
328 };
329 return match executor {
330 Executor::MultiThread(pool) => pool.install(run),
331 _ => run(),
332 };
333 }
334 }
335 }
336 }
337
338 if n_blocks == 1 {
342 return eval_fn(&mut a.view(), &b.view());
343 }
344 (0..n_blocks).try_for_each(|block| call(block, 0, period))
345}
346
347pub struct BinChainStep<'a> {
350 pub eval_fn: &'a BinFn,
351 pub b: &'a Tensor,
352 pub period: usize,
353 pub share: BShare,
354}
355
356fn chain_workers(len: usize) -> usize {
357 #[cfg(feature = "multithread-mm")]
358 {
359 if len < current_threading_element_threshold() {
361 return 1;
362 }
363 match current_tract_executor() {
364 Executor::MultiThread(pool) => pool.current_num_threads(),
365 Executor::RayonGlobal => rayon::current_num_threads(),
366 Executor::SingleThread => 1,
367 }
368 }
369 #[cfg(not(feature = "multithread-mm"))]
370 {
371 let _ = len;
372 1
373 }
374}
375
376pub fn par_bin_chain(steps: &[BinChainStep], a: &mut Tensor) -> TractResult<()> {
389 const CHAIN_CHUNK: usize = 16384;
391 if steps.is_empty() || a.len() == 0 {
392 return Ok(());
393 }
394 let a_len = a.len();
395 let a_item = a.datum_type().size_of() as isize;
396 for s in steps {
397 ensure!(
398 a_len.is_multiple_of(s.period),
399 "par_bin_chain: period {} does not divide a.len() {}",
400 s.period,
401 a_len
402 );
403 match s.share {
404 BShare::Lockstep => ensure!(
405 s.b.len() == s.period,
406 "par_bin_chain: Lockstep wants b.len() {} == period {}",
407 s.b.len(),
408 s.period
409 ),
410 BShare::PerBlock => ensure!(
411 s.b.len() == a_len / s.period,
412 "par_bin_chain: PerBlock wants b.len() {} == {} blocks",
413 s.b.len(),
414 a_len / s.period
415 ),
416 }
417 }
418 let workers = chain_workers(a_len);
419 let max_chunk = if workers > 1 { CHAIN_CHUNK.min(a_len / (4 * workers)) } else { CHAIN_CHUNK }
420 .max(4 * vector_size());
421 let tiled = steps
425 .iter()
426 .map(|s| {
427 if s.share != BShare::Lockstep || s.period * 2 > max_chunk {
428 return Ok(None);
429 }
430 let blocks = a_len / s.period;
431 let k = (1..=blocks.min(max_chunk / s.period))
432 .rev()
433 .find(|k| blocks.is_multiple_of(*k))
434 .unwrap_or(1);
435 if k == 1 {
436 return Ok(None);
437 }
438 Ok(Some(
439 s.b.clone()
440 .into_shape(&[s.period])?
441 .broadcast_to_shape(&[k, s.period])?
442 .into_shape(&[k * s.period])?,
443 ))
444 })
445 .collect::<TractResult<Vec<Option<Tensor>>>>()?;
446 let chain_b = |ix: usize| tiled[ix].as_ref().unwrap_or(steps[ix].b);
447 let chain_period = |ix: usize| tiled[ix].as_ref().map_or(steps[ix].period, |t| t.len());
448 let unit = (0..steps.len()).fold(a_len, |u, ix| u.gcd(&chain_period(ix)));
449 if unit % vector_size() != 0 {
450 for s in steps {
451 par_bin(s.eval_fn, a, s.b, s.period, s.share)?;
452 }
453 return Ok(());
454 }
455 let q = unit / vector_size();
456 let mut t = 1;
459 let mut d = 1;
460 while d * d <= q {
461 if q % d == 0 {
462 for cand in [d, q / d] {
463 if cand <= max_chunk / vector_size() {
464 t = t.max(cand);
465 }
466 }
467 }
468 d += 1;
469 }
470 let chunk = t * vector_size();
471 if chunk < 4 * vector_size() {
472 for s in steps {
473 par_bin(s.eval_fn, a, s.b, s.period, s.share)?;
474 }
475 return Ok(());
476 }
477 let n_chunks = a_len / chunk;
478 let a = &*a;
479 static STRIDES: [isize; 1] = [1];
480 let a_shape = [chunk];
481 let call = |ix: usize| -> TractResult<()> {
486 let g = ix * chunk;
487 unsafe {
488 let mut a_chunk = TensorView::from_bytes(a, g as isize * a_item, &a_shape, &STRIDES);
489 for (sx, s) in steps.iter().enumerate() {
490 let (b, period) = (chain_b(sx), chain_period(sx));
491 let b_item = b.datum_type().size_of() as isize;
492 let (b_offset, b_shape) = match s.share {
493 BShare::Lockstep => ((g % period) as isize * b_item, [chunk]),
494 BShare::PerBlock => ((g / period) as isize * b_item, [1]),
495 };
496 let b_chunk = TensorView::from_bytes(b, b_offset, &b_shape, &STRIDES);
497 (s.eval_fn)(&mut a_chunk, &b_chunk)?;
498 }
499 Ok(())
500 }
501 };
502
503 #[cfg(feature = "multithread-mm")]
504 {
505 use rayon::prelude::*;
506 if a_len >= current_threading_element_threshold() && n_chunks > 1 {
507 let executor = current_tract_executor();
508 let nth = match &executor {
509 Executor::MultiThread(pool) => pool.current_num_threads(),
510 Executor::RayonGlobal => rayon::current_num_threads(),
511 Executor::SingleThread => 1,
512 };
513 if nth > 1 {
514 let per_worker = n_chunks.div_ceil(nth);
515 let n_workers = n_chunks.div_ceil(per_worker);
516 let run = || {
517 (0..n_workers).into_par_iter().try_for_each(|w| {
518 let first = w * per_worker;
519 (first..(first + per_worker).min(n_chunks)).try_for_each(&call)
520 })
521 };
522 return match executor {
523 Executor::MultiThread(pool) => pool.install(run),
524 _ => run(),
525 };
526 }
527 }
528 }
529 (0..n_chunks).try_for_each(call)
530}
531
532#[cfg(all(test, feature = "multithread-mm", not(target_os = "wasi")))]
535mod chain_tests {
536 use super::{BShare, BinChainStep, Executor, multithread_tract_scope, par_bin, par_bin_chain};
537 use crate::BinFn;
538 use tract_data::internal::{Tensor, TensorView, TractResult};
539
540 fn add() -> impl Fn(&mut TensorView, &TensorView) -> TractResult<()> + Send + Sync {
541 |a: &mut TensorView, b: &TensorView| {
542 let av = a.as_slice_mut::<f32>()?;
543 let bv = b.as_slice::<f32>()?;
544 if bv.len() == 1 {
545 av.iter_mut().for_each(|x| *x += bv[0]);
546 } else {
547 av.iter_mut().zip(bv).for_each(|(x, y)| *x += *y);
548 }
549 Ok(())
550 }
551 }
552
553 fn mul() -> impl Fn(&mut TensorView, &TensorView) -> TractResult<()> + Send + Sync {
554 |a: &mut TensorView, b: &TensorView| {
555 let av = a.as_slice_mut::<f32>()?;
556 let bv = b.as_slice::<f32>()?;
557 if bv.len() == 1 {
558 av.iter_mut().for_each(|x| *x *= bv[0]);
559 } else {
560 av.iter_mut().zip(bv).for_each(|(x, y)| *x *= *y);
561 }
562 Ok(())
563 }
564 }
565
566 #[test]
570 fn chain_matches_sequential_par_bin() {
571 let add = add();
572 let mul = mul();
573 let len = 12288usize;
574 for periods in [[768usize, 256, 256], [768, 24, 768], [96, 96, 12288]] {
575 let b0 = Tensor::from_shape(&[1, 1, periods[0]], &vec![2f32; periods[0]]).unwrap();
576 let b1 = Tensor::from_shape(&[periods[1]], &vec![3f32; periods[1]]).unwrap();
577 let b2 =
578 Tensor::from_shape(&[len / periods[2]], &vec![0.5f32; len / periods[2]]).unwrap();
579 let steps = vec![
580 BinChainStep {
581 eval_fn: &add as &BinFn,
582 b: &b0,
583 period: periods[0],
584 share: BShare::Lockstep,
585 },
586 BinChainStep {
587 eval_fn: &mul as &BinFn,
588 b: &b1,
589 period: periods[1],
590 share: BShare::Lockstep,
591 },
592 BinChainStep {
593 eval_fn: &add as &BinFn,
594 b: &b2,
595 period: periods[2],
596 share: BShare::PerBlock,
597 },
598 ];
599 let input = Tensor::from_shape(&[len], &(0..len).map(|i| i as f32).collect::<Vec<_>>())
600 .unwrap();
601 let mut reference = input.clone();
602 for s in &steps {
603 par_bin(s.eval_fn, &mut reference, s.b, s.period, s.share).unwrap();
604 }
605 for nth in [0usize, 4] {
606 let mut a = input.clone();
607 if nth == 0 {
608 par_bin_chain(&steps, &mut a).unwrap();
609 } else {
610 #[cfg(target_family = "wasm")]
611 let pool = Executor::RayonGlobal;
612 #[cfg(not(target_family = "wasm"))]
613 let pool = Executor::multithread(nth);
614 multithread_tract_scope(pool, || par_bin_chain(&steps, &mut a).unwrap());
615 }
616 let got = a.view().as_slice::<f32>().unwrap();
617 let want = reference.view().as_slice::<f32>().unwrap();
618 assert_eq!(got, want, "len={len} periods={periods:?} nth={nth}");
619 }
620 }
621 }
622}