1use crate::ParShape;
11use core::num::NonZeroUsize;
12use core::time::Duration;
13
14#[cfg(feature = "threadteam")]
15pub use threadteam::{PAYS, pays_at, width_for_by};
16
17#[cfg(not(feature = "threadteam"))]
19pub const PAYS: f64 = 3.0;
20
21#[cfg(not(feature = "threadteam"))]
23pub fn pays_at(estimate: Duration, sections: u32, dispatch_cost: Duration) -> bool {
24 if dispatch_cost == Duration::MAX {
25 return false;
26 }
27 if sections == 0 {
28 return true;
29 }
30 estimate.as_secs_f64() >= PAYS * f64::from(sections) * dispatch_cost.as_secs_f64()
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum HostClass {
45 X86_64Avx512,
47 X86_64Avx2,
49 X86_64Scalar,
51 Aarch64Apple,
53 Aarch64,
55 Generic,
57}
58
59impl HostClass {
60 pub const ALL: [HostClass; 6] = [
62 HostClass::X86_64Avx512,
63 HostClass::X86_64Avx2,
64 HostClass::X86_64Scalar,
65 HostClass::Aarch64Apple,
66 HostClass::Aarch64,
67 HostClass::Generic,
68 ];
69
70 pub fn detect() -> Self {
73 use core::sync::atomic::{AtomicU8, Ordering};
74 static CLASS: AtomicU8 = AtomicU8::new(0);
75 let c = CLASS.load(Ordering::Relaxed);
76 if c != 0 {
77 return Self::ALL[c as usize - 1];
78 }
79 let class = Self::detect_uncached();
80 let idx = Self::ALL.iter().position(|&k| k == class).unwrap();
81 CLASS.store(idx as u8 + 1, Ordering::Relaxed);
82 class
83 }
84
85 #[cfg(all(target_arch = "x86_64", feature = "std"))]
86 fn detect_uncached() -> Self {
87 if std::arch::is_x86_feature_detected!("avx512f") {
88 HostClass::X86_64Avx512
89 } else if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
90 HostClass::X86_64Avx2
91 } else {
92 HostClass::X86_64Scalar
93 }
94 }
95
96 #[cfg(all(target_arch = "x86_64", not(feature = "std")))]
97 fn detect_uncached() -> Self {
98 if cfg!(target_feature = "avx512f") {
99 HostClass::X86_64Avx512
100 } else if cfg!(all(target_feature = "avx2", target_feature = "fma")) {
101 HostClass::X86_64Avx2
102 } else {
103 HostClass::X86_64Scalar
104 }
105 }
106
107 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
108 fn detect_uncached() -> Self {
109 HostClass::Aarch64Apple
110 }
111
112 #[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
113 fn detect_uncached() -> Self {
114 HostClass::Aarch64
115 }
116
117 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
118 fn detect_uncached() -> Self {
119 HostClass::Generic
120 }
121
122 pub const fn name(self) -> &'static str {
124 match self {
125 HostClass::X86_64Avx512 => "x86_64-avx512",
126 HostClass::X86_64Avx2 => "x86_64-avx2",
127 HostClass::X86_64Scalar => "x86_64-scalar",
128 HostClass::Aarch64Apple => "aarch64-apple",
129 HostClass::Aarch64 => "aarch64",
130 HostClass::Generic => "generic",
131 }
132 }
133}
134
135#[cfg(any(all(feature = "rayon", not(feature = "threadteam")), test))]
147#[cfg_attr(feature = "threadteam", allow(dead_code))]
148pub(crate) fn outside_cost_of(class: HostClass, w: NonZeroUsize) -> Duration {
149 width_model(outside_anchors(class), w)
150}
151
152#[cfg(any(all(feature = "rayon", not(feature = "threadteam")), test))]
153#[cfg_attr(feature = "threadteam", allow(dead_code))]
154const fn outside_anchors(class: HostClass) -> [u64; 3] {
155 match class {
156 HostClass::X86_64Avx512 => [9_700, 10_100, 8_700],
157 HostClass::Aarch64Apple => [14_000, 18_000, 19_000],
158 _ => [40_000; 3],
159 }
160}
161
162#[cfg(any(all(feature = "rayon", not(feature = "threadteam")), test))]
177#[cfg_attr(feature = "threadteam", allow(dead_code))]
178pub(crate) fn in_pool_cost_of(class: HostClass, w: NonZeroUsize) -> Duration {
179 width_model(in_pool_anchors(class), w)
180}
181
182#[cfg(any(all(feature = "rayon", not(feature = "threadteam")), test))]
183#[cfg_attr(feature = "threadteam", allow(dead_code))]
184const fn in_pool_anchors(class: HostClass) -> [u64; 3] {
185 match class {
186 HostClass::X86_64Avx512 => [3_000, 3_600, 3_500],
187 HostClass::Aarch64Apple => [6_000, 11_000, 5_500],
188 _ => [15_000; 3],
189 }
190}
191
192#[cfg(any(all(feature = "rayon", not(feature = "threadteam")), test))]
195#[cfg_attr(feature = "threadteam", allow(dead_code))]
196fn width_model(anchors: [u64; 3], w: NonZeroUsize) -> Duration {
197 let w = w.get() as i64;
198 let ns = if w <= 2 {
199 anchors[0] as i64
200 } else if w >= 6 {
201 anchors[2] as i64
202 } else {
203 let i = (w > 4) as usize;
204 let (w0, w1) = (2 + 2 * i as i64, 4 + 2 * i as i64);
205 let (c0, c1) = (anchors[i] as i64, anchors[i + 1] as i64);
206 c0 + (c1 - c0) * (w - w0) / (w1 - w0)
207 };
208 Duration::from_nanos(ns as u64)
209}
210
211#[cfg(feature = "rayon")]
216#[inline]
217pub(crate) fn caller_in_pool() -> bool {
218 rayon::current_thread_index().is_some()
219}
220
221#[cfg(all(feature = "rayon", not(feature = "threadteam")))]
223pub(crate) fn rayon_cost(w: NonZeroUsize) -> Duration {
224 if caller_in_pool() {
225 in_pool_cost_of(HostClass::detect(), w)
226 } else {
227 outside_cost_of(HostClass::detect(), w)
228 }
229}
230
231#[cfg(all(feature = "rayon", not(feature = "threadteam")))]
235pub(crate) fn min_rayon_cost() -> Duration {
236 let class = HostClass::detect();
237 let anchors = if caller_in_pool() { in_pool_anchors(class) } else { outside_anchors(class) };
238 Duration::from_nanos(anchors[0].min(anchors[1]).min(anchors[2]))
239}
240
241pub mod ns_per_mac {
244 use super::rates;
245 pub fn f64() -> f64 {
247 rates().gemm_mac.real
248 }
249 pub fn c64() -> f64 {
251 rates().gemm_mac.cplx
252 }
253 pub fn f32() -> f64 {
255 rates().gemm_single.real
256 }
257 pub fn c32() -> f64 {
259 rates().gemm_single.cplx
260 }
261}
262
263pub const NS_PER_SWAP: f64 = 0.55;
269
270#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
278pub enum ParThreshold {
279 #[default]
281 Derived,
282 Fixed(usize),
287 Never,
289}
290
291impl From<usize> for ParThreshold {
295 fn from(value: usize) -> Self {
296 match value {
297 0 => ParThreshold::Derived,
298 usize::MAX => ParThreshold::Never,
299 n => ParThreshold::Fixed(n),
300 }
301 }
302}
303
304impl From<ParThreshold> for usize {
307 fn from(value: ParThreshold) -> Self {
308 match value {
309 ParThreshold::Derived => 0,
310 ParThreshold::Fixed(n) => n.max(1),
311 ParThreshold::Never => usize::MAX,
312 }
313 }
314}
315
316impl ParThreshold {
317 #[cfg_attr(feature = "par-trace", track_caller)]
323 pub(crate) fn region_pays<T: crate::ComplexField>(
324 self,
325 shape: ParShape,
326 units: usize,
327 rate: Rate,
328 sections: u32,
329 ) -> bool {
330 if shape.degree() < 2 && !matches!(self, Self::Fixed(_)) {
333 return false;
334 }
335 let r = self.region_pays_impl::<T>(shape, units, rate, sections);
336 #[cfg(feature = "par-trace")]
337 trace_decision(shape, shape.degree(), units, rate.of::<T>(), sections, r, core::panic::Location::caller());
338 r
339 }
340
341 fn region_pays_impl<T: crate::ComplexField>(
342 self,
343 shape: ParShape,
344 units: usize,
345 rate: Rate,
346 sections: u32,
347 ) -> bool {
348 match self {
349 Self::Never => false,
350 Self::Fixed(floor) => units >= floor,
351 Self::Derived => {
352 shape.pays_full(serial_time(units, rate.of::<T>()), sections)
353 },
354 }
355 }
356}
357
358#[derive(Copy, Clone, Debug, PartialEq)]
364pub struct Rate {
365 pub real: f64,
367 pub cplx: f64,
369}
370
371impl Rate {
372 pub fn of<T: crate::ComplexField>(self) -> f64 {
374 if const { T::IS_NATIVE_F32 } {
375 self.real / 2.0
376 } else if const { T::IS_NATIVE_C32 } {
377 self.cplx / 2.0
378 } else if const { T::IS_REAL } {
379 self.real
380 } else {
381 self.cplx
382 }
383 }
384}
385
386#[derive(Copy, Clone, Debug, PartialEq)]
408pub struct Rates {
409 pub gemm_mac: Rate,
412 pub gemm_single: Rate,
414 pub block_apply_mac: Rate,
416 pub lu_full_pivot: Rate,
419 pub bunch_kaufman: Rate,
422 pub qr_col_pivot: Rate,
425 pub bidiag: Rate,
428 pub hessenberg: Rate,
431 pub tridiag: Rate,
434 pub matvec: Rate,
437 pub bidiag_svd_dc: Rate,
444 pub tridiag_evd_dc: Rate,
448 pub triangular_inverse: Rate,
453 pub sparse_matmul: Rate,
456 pub householder_rank_one: Rate,
458 pub lu_swap: Rate,
462}
463
464const fn fmin(a: f64, b: f64) -> f64 {
465 if b < a { b } else { a }
466}
467
468const fn rmin(a: Rate, b: Rate) -> Rate {
469 Rate { real: fmin(a.real, b.real), cplx: fmin(a.cplx, b.cplx) }
470}
471
472const fn r(real: f64, cplx: f64) -> Rate {
473 Rate { real, cplx }
474}
475
476impl Rates {
477 const fn min_of(a: &Rates, b: &Rates) -> Rates {
479 Rates {
480 gemm_mac: rmin(a.gemm_mac, b.gemm_mac),
481 gemm_single: rmin(a.gemm_single, b.gemm_single),
482 block_apply_mac: rmin(a.block_apply_mac, b.block_apply_mac),
483 lu_full_pivot: rmin(a.lu_full_pivot, b.lu_full_pivot),
484 bunch_kaufman: rmin(a.bunch_kaufman, b.bunch_kaufman),
485 qr_col_pivot: rmin(a.qr_col_pivot, b.qr_col_pivot),
486 bidiag: rmin(a.bidiag, b.bidiag),
487 hessenberg: rmin(a.hessenberg, b.hessenberg),
488 tridiag: rmin(a.tridiag, b.tridiag),
489 matvec: rmin(a.matvec, b.matvec),
490 bidiag_svd_dc: rmin(a.bidiag_svd_dc, b.bidiag_svd_dc),
491 tridiag_evd_dc: rmin(a.tridiag_evd_dc, b.tridiag_evd_dc),
492 triangular_inverse: rmin(a.triangular_inverse, b.triangular_inverse),
493 sparse_matmul: rmin(a.sparse_matmul, b.sparse_matmul),
494 householder_rank_one: rmin(a.householder_rank_one, b.householder_rank_one),
495 lu_swap: rmin(a.lu_swap, b.lu_swap),
496 }
497 }
498
499 pub const X86_64_AVX512: Rates = Rates {
501 gemm_mac: r(0.0641, 0.2565),
502 gemm_single: r(0.0320, 0.1283),
503 block_apply_mac: r(0.0676, 0.2685),
504 lu_full_pivot: r(0.4011, 0.8679),
505 bunch_kaufman: r(0.8295, 1.3904),
506 qr_col_pivot: r(0.3645, 1.0320),
507 bidiag: r(1.0427, 2.1997),
508 hessenberg: r(0.8576, 2.2212),
509 tridiag: r(0.7695, 1.9252),
510 matvec: r(0.1654, 0.4215),
512 bidiag_svd_dc: r(152.9, 152.9),
513 tridiag_evd_dc: r(97.4, 97.4),
514 triangular_inverse: r(0.01936, 0.07228),
515 sparse_matmul: r(4.1315, 5.2342),
517 householder_rank_one: r(0.4783, 1.0303),
518 lu_swap: r(0.98, 1.30),
522 };
523
524 pub const AARCH64_APPLE: Rates = Rates {
526 gemm_mac: r(0.037, 0.165),
527 gemm_single: r(0.037 / 2.0, 0.165 / 2.0),
528 block_apply_mac: r(0.037, 0.165),
529 lu_full_pivot: r(0.216, 0.6),
530 bunch_kaufman: r(0.238, 0.38),
531 qr_col_pivot: r(0.202, 0.713),
532 bidiag: r(0.48, 1.36),
533 hessenberg: r(0.451, 1.57),
534 tridiag: r(0.339, 1.16),
535 matvec: r(0.1, 0.34),
536 bidiag_svd_dc: r(53.0, 53.0),
537 tridiag_evd_dc: r(31.0, 31.0),
538 triangular_inverse: r(0.0096, 0.0344),
539 sparse_matmul: r(1.5, 1.8),
540 householder_rank_one: r(0.212, 0.725),
541 lu_swap: r(NS_PER_SWAP, NS_PER_SWAP),
542 };
543
544 pub const GENERIC: Rates = Rates::min_of(&Rates::X86_64_AVX512, &Rates::AARCH64_APPLE);
546
547 pub fn for_class(class: HostClass) -> &'static Rates {
549 static AVX512: Rates = Rates::X86_64_AVX512;
550 static APPLE: Rates = Rates::AARCH64_APPLE;
551 static GENERIC: Rates = Rates::GENERIC;
552 match class {
553 HostClass::X86_64Avx512 => &AVX512,
554 HostClass::Aarch64Apple => &APPLE,
555 HostClass::X86_64Avx2 | HostClass::X86_64Scalar | HostClass::Aarch64 | HostClass::Generic => &GENERIC,
556 }
557 }
558}
559
560pub fn rates() -> &'static Rates {
562 Rates::for_class(HostClass::detect())
563}
564
565pub mod rate {
568 use super::{Rate, rates};
569 macro_rules! families {
570 ($($name:ident),* $(,)?) => {$(
571 #[doc = core::concat!("`Rates::", core::stringify!($name), "` of the host class.")]
572 pub fn $name() -> Rate { rates().$name }
573 )*};
574 }
575 families!(
576 gemm_mac,
577 block_apply_mac,
578 lu_full_pivot,
579 bunch_kaufman,
580 qr_col_pivot,
581 bidiag,
582 hessenberg,
583 tridiag,
584 matvec,
585 bidiag_svd_dc,
586 tridiag_evd_dc,
587 triangular_inverse,
588 sparse_matmul,
589 householder_rank_one,
590 lu_swap,
591 );
592}
593
594pub(crate) fn gemm_ns_per_mac<T: crate::ComplexField>() -> f64 {
598 if const { T::IS_NATIVE_F32 } {
599 ns_per_mac::f32()
600 } else if const { T::IS_NATIVE_C32 } {
601 ns_per_mac::c32()
602 } else if const { T::IS_REAL } {
603 ns_per_mac::f64()
604 } else {
605 ns_per_mac::c64()
606 }
607}
608
609
610#[cfg(feature = "par-trace")]
614pub mod trace {
615 use std::sync::Mutex;
616 use std::sync::atomic::{AtomicBool, Ordering};
617 #[derive(Clone, Debug)]
619 pub struct Record {
620 pub site: &'static str,
622 pub dims: [usize; 3],
624 pub units: usize,
626 pub sections: u32,
628 pub estimate_ns: f64,
630 pub dispatch_ns: f64,
632 pub parallel: bool,
634 }
635 pub static N_TASKS_MODE: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
638 static ON: AtomicBool = AtomicBool::new(false);
639 static LOG: Mutex<Vec<Record>> = Mutex::new(Vec::new());
640 std::thread_local! {
641 static DIMS: core::cell::Cell<(&'static str, [usize; 3])> = const { core::cell::Cell::new(("", [0; 3])) };
642 }
643 pub fn enable(on: bool) {
645 if on {
646 LOG.lock().unwrap().clear();
647 }
648 ON.store(on, Ordering::SeqCst);
649 }
650 pub fn take() -> Vec<Record> {
652 core::mem::take(&mut *LOG.lock().unwrap())
653 }
654 pub fn shape(label: &'static str, a: usize, b: usize, c: usize) {
656 DIMS.with(|d| d.set((label, [a, b, c])));
657 }
658 pub(crate) fn record(
659 loc: &'static core::panic::Location<'static>,
660 units: usize,
661 sections: u32,
662 estimate_ns: f64,
663 dispatch_ns: f64,
664 parallel: bool,
665 ) {
666 if !ON.load(Ordering::Relaxed) {
667 return;
668 }
669 let (label, dims) = DIMS.with(|d| d.replace(("", [0; 3])));
670 let site: &'static str = if label.is_empty() {
671 Box::leak(format!("{}:{}", loc.file().rsplit('/').next().unwrap(), loc.line()).into_boxed_str())
672 } else {
673 label
674 };
675 LOG.lock().unwrap().push(Record { site, dims, units, sections, estimate_ns, dispatch_ns, parallel });
676 }
677}
678
679pub(crate) fn par_pays_time(par: &crate::Par, estimate: Duration, sections: u32, width: usize) -> bool {
684 let width = width.min(par.degree());
685 let Some(width) = NonZeroUsize::new(width).filter(|w| w.get() >= 2) else {
686 return false;
687 };
688 match par {
689 crate::Par::Seq => false,
690 #[cfg(feature = "threadteam")]
691 crate::Par::Team(team) => team.pays(estimate, sections, width),
692 other => other.shape().pays(estimate, sections, width),
693 }
694}
695
696#[cfg(all(feature = "rayon", feature = "threadteam"))]
698fn rayon_kind() -> threadteam::DispatchKind {
699 if caller_in_pool() {
700 threadteam::DispatchKind::InPool
701 } else {
702 threadteam::DispatchKind::Outside
703 }
704}
705
706#[cfg(all(feature = "rayon", feature = "threadteam"))]
708fn host_costs() -> &'static threadteam::DispatchCosts {
709 static HOST: std::sync::OnceLock<threadteam::DispatchCosts> = std::sync::OnceLock::new();
710 HOST.get_or_init(threadteam::DispatchCosts::host_default)
711}
712
713pub(crate) fn par_can_pay_time(par: &crate::Par, estimate: Duration, sections: u32) -> bool {
718 if par.degree() < 2 {
719 return false;
720 }
721 let min = match par {
722 crate::Par::Seq => Duration::MAX,
723 #[cfg(feature = "threadteam")]
724 crate::Par::Team(team) => return par_can_pay_time_team(team, estimate, sections),
725 other => other.shape().min_dispatch_cost(),
726 };
727 pays_at(estimate, sections, min)
728}
729
730#[cfg(feature = "threadteam")]
732pub(crate) fn par_can_pay_time_team(team: &threadteam::TeamExec<'_>, estimate: Duration, sections: u32) -> bool {
733 team.size().get() >= 2
734 && pays_at(estimate, sections, team.dispatch_costs().min_cost(threadteam::DispatchKind::Team))
735}
736
737#[cfg_attr(feature = "par-trace", track_caller)]
740pub(crate) fn par_pays<T: crate::ComplexField>(
741 par: &crate::Par,
742 units: usize,
743 rate: Rate,
744 sections: u32,
745) -> bool {
746 if par.degree() < 2 {
749 return false;
750 }
751 let r = par_pays_time(par, serial_time(units, rate.of::<T>()), sections, par.degree());
752 #[cfg(feature = "par-trace")]
753 trace_decision(par.shape(), par.shape().degree(), units, rate.of::<T>(), sections, r, core::panic::Location::caller());
754 r
755}
756
757#[cfg(feature = "par-trace")]
758fn trace_decision(
759 shape: ParShape,
760 width: usize,
761 units: usize,
762 ns: f64,
763 sections: u32,
764 r: bool,
765 loc: &'static core::panic::Location<'static>,
766) {
767 let d = NonZeroUsize::new(width)
768 .map(|w| shape.dispatch_cost(w).as_secs_f64() * 1e9)
769 .unwrap_or(f64::INFINITY);
770 trace::record(loc, units, sections, units as f64 * ns, d, r);
771}
772
773#[cfg_attr(feature = "par-trace", track_caller)]
778pub(crate) fn join_pays<T: crate::ComplexField>(
779 par: &crate::Par,
780 units: usize,
781 rate: Rate,
782 sections: u32,
783) -> bool {
784 if par.degree() < 2 {
785 return false;
786 }
787 let r = par_pays_time(par, serial_time(units, rate.of::<T>()), sections, 2);
788 #[cfg(feature = "par-trace")]
789 trace_decision(par.shape(), 2, units, rate.of::<T>(), sections, r, core::panic::Location::caller());
790 r
791}
792
793pub(crate) fn halves_pays<T: crate::ComplexField>(
801 par: &crate::Par,
802 n: usize,
803 rate: Rate,
804 power: u32,
805) -> bool {
806 let h = n / 2;
807 join_pays::<T>(par, h.saturating_pow(power).saturating_mul(2), rate, 1)
808}
809
810pub(crate) fn block_apply_estimate<T: crate::ComplexField>(
818 m: usize,
819 n: usize,
820 k: usize,
821 products: usize,
822) -> Duration {
823 block_apply_estimate_with::<T>(rates(), m, n, k, products)
824}
825
826pub(crate) fn block_apply_estimate_with<T: crate::ComplexField>(
828 rt: &Rates,
829 m: usize,
830 n: usize,
831 k: usize,
832 products: usize,
833) -> Duration {
834 let macs = m.saturating_mul(n).saturating_mul(k).saturating_mul(products);
835 let ns = macs as f64 * rt.block_apply_mac.of::<T>() + m.saturating_mul(k) as f64 * rt.householder_rank_one.of::<T>();
837 Duration::from_secs_f64(ns * 1e-9)
838}
839
840pub(crate) fn serial_time(count: usize, ns: f64) -> Duration {
842 Duration::from_secs_f64(count as f64 * ns * 1e-9)
843}
844
845impl ParShape {
846 pub fn dispatch_cost(self, width: NonZeroUsize) -> Duration {
858 let width = width.min(NonZeroUsize::new(self.degree().max(1)).unwrap());
859 if width.get() < 2 {
860 return Duration::MAX;
861 }
862 match self {
863 Self::Seq => Duration::MAX,
864 #[cfg(feature = "rayon")]
865 Self::Rayon(_) => {
866 #[cfg(feature = "threadteam")]
867 {
868 host_costs().cost(rayon_kind(), width)
869 }
870 #[cfg(not(feature = "threadteam"))]
871 {
872 rayon_cost(width)
873 }
874 },
875 #[cfg(feature = "threadteam")]
876 Self::Team(_, costs) => costs.cost(threadteam::DispatchKind::Team, width),
877 }
878 }
879
880 pub fn min_dispatch_cost(self) -> Duration {
884 if self.degree() < 2 {
885 return Duration::MAX;
886 }
887 match self {
888 Self::Seq => Duration::MAX,
889 #[cfg(feature = "rayon")]
890 Self::Rayon(_) => {
891 #[cfg(feature = "threadteam")]
892 {
893 host_costs().min_cost(rayon_kind())
894 }
895 #[cfg(not(feature = "threadteam"))]
896 {
897 min_rayon_cost()
898 }
899 },
900 #[cfg(feature = "threadteam")]
901 Self::Team(_, costs) => costs.min_cost(threadteam::DispatchKind::Team),
902 }
903 }
904
905 pub fn pays(self, estimate: Duration, sections: u32, width: NonZeroUsize) -> bool {
909 pays_at(estimate, sections, self.dispatch_cost(width))
910 }
911
912 pub fn pays_full(self, estimate: Duration, sections: u32) -> bool {
914 match NonZeroUsize::new(self.degree()) {
915 Some(width) => self.pays(estimate, sections, width),
916 None => false,
917 }
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924 use crate::c64;
925
926 fn nz(n: usize) -> NonZeroUsize {
927 NonZeroUsize::new(n).unwrap()
928 }
929
930 #[test]
931 fn seq_never_pays() {
932 assert_eq!(ParShape::Seq.dispatch_cost(nz(4)), Duration::MAX);
933 assert!(!ParShape::Seq.pays(Duration::from_secs(10), 1, nz(4)));
934 assert!(!ParShape::Seq.pays_full(Duration::from_secs(10), 1));
935 }
936
937 #[test]
938 #[cfg(feature = "threadteam")]
939 fn min_dispatch_cost_is_the_cheapest_width() {
940 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
941 let us = Duration::from_micros;
942 for t in [[us(9), us(5), us(3)], [us(4), us(4), us(4)], [us(2), us(7), us(12)]] {
944 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic).with(DispatchKind::Team, WidthCosts::new(t));
945 let s = ParShape::Team(nz(6), costs);
946 let want = (2..=6).map(|w| s.dispatch_cost(nz(w))).min().unwrap();
947 assert_eq!(s.min_dispatch_cost(), want, "{t:?}");
948 }
949 assert_eq!(ParShape::Seq.min_dispatch_cost(), Duration::MAX);
950 assert_eq!(ParShape::Team(nz(1), DispatchCosts::for_class(threadteam::HostClass::Generic)).min_dispatch_cost(), Duration::MAX);
951 }
952
953 #[test]
954 fn pays_is_the_multiple_of_the_cost() {
955 let d = Duration::from_micros(2);
956 assert!(pays_at(Duration::from_micros(6), 1, d));
957 assert!(!pays_at(Duration::from_nanos(5999), 1, d));
958 assert!(!pays_at(Duration::from_micros(11), 2, d));
959 assert!(pays_at(Duration::from_micros(12), 2, d));
960 assert!(!pays_at(Duration::from_secs(1), 1, Duration::MAX));
961 }
962
963 #[cfg(feature = "rayon")]
964 #[test]
965 fn rayon_uses_the_outside_cost() {
966 let s = ParShape::Rayon(nz(6));
967 assert_eq!(s.dispatch_cost(nz(1)), Duration::MAX);
968 let d6 = s.dispatch_cost(nz(6));
969 assert!(d6 > Duration::ZERO && d6 < Duration::MAX);
972 assert_eq!(s.dispatch_cost(nz(60)), d6);
974 assert!(!s.pays(d6, 1, nz(6)));
976 assert!(s.pays(d6 * 4, 1, nz(6)));
977 }
978
979 #[cfg(feature = "threadteam")]
980 #[test]
981 fn team_shape_carries_the_costs_of_its_token() {
982 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
983 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic)
984 .with(DispatchKind::Team, WidthCosts::flat(Duration::from_micros(5)));
985 let s = ParShape::Team(nz(4), costs);
986 assert_eq!(s.dispatch_cost(nz(4)), Duration::from_micros(5));
987 assert_eq!(s.dispatch_cost(nz(1)), Duration::MAX);
988 assert!(s.pays(Duration::from_micros(15), 1, nz(4)));
989 assert!(!s.pays(Duration::from_micros(14), 1, nz(4)));
990 }
991
992 #[test]
993 fn gemm_rates_are_per_dtype() {
994 assert_eq!(gemm_ns_per_mac::<f64>(), ns_per_mac::f64());
995 assert_eq!(gemm_ns_per_mac::<crate::c64>(), ns_per_mac::c64());
996 assert_eq!(gemm_ns_per_mac::<f32>(), ns_per_mac::f32());
997 assert_eq!(gemm_ns_per_mac::<crate::c32>(), ns_per_mac::c32());
998 assert!(ns_per_mac::c64() > 4.0 * ns_per_mac::f64());
1000 }
1001
1002 fn floor_units<T: crate::ComplexField>(
1004 shape: ParShape,
1005 r: Rate,
1006 sections: u32,
1007 ) -> usize {
1008 let pays = |u: usize| {
1009 ParThreshold::Derived.region_pays::<T>(shape, u, r, sections)
1010 };
1011 let mut hi = 1usize;
1012 while !pays(hi) {
1013 hi *= 2;
1014 assert!(hi < 1 << 40, "never pays");
1015 }
1016 let mut lo = hi / 2;
1017 while hi - lo > 1 {
1018 let mid = (lo + hi) / 2;
1019 if pays(mid) {
1020 hi = mid;
1021 } else {
1022 lo = mid;
1023 }
1024 }
1025 hi
1026 }
1027
1028 fn gates(rt: &Rates) -> [(&'static str, Rate, u32); 10] {
1030 [
1031 ("lu_full_pivot", rt.lu_full_pivot, 1),
1032 ("bunch_kaufman", rt.bunch_kaufman, 1),
1033 ("qr_col_pivot", rt.qr_col_pivot, 1),
1034 ("bidiag", rt.bidiag, 2),
1035 ("hessenberg", rt.hessenberg, 2),
1036 ("tridiag", rt.tridiag, 1),
1037 ("matvec", rt.matvec, 1),
1038 ("householder_rank_one", rt.householder_rank_one, 1),
1039 ("gemm_mac", rt.gemm_mac, 1),
1040 ("sparse_matmul", rt.sparse_matmul, 1),
1041 ]
1042 }
1043
1044 const M5: &Rates = &Rates::AARCH64_APPLE;
1047
1048 fn entries(rt: &Rates) -> [(&'static str, Rate); 16] {
1050 [
1051 ("gemm_mac", rt.gemm_mac),
1052 ("gemm_single", rt.gemm_single),
1053 ("block_apply_mac", rt.block_apply_mac),
1054 ("lu_full_pivot", rt.lu_full_pivot),
1055 ("bunch_kaufman", rt.bunch_kaufman),
1056 ("qr_col_pivot", rt.qr_col_pivot),
1057 ("bidiag", rt.bidiag),
1058 ("hessenberg", rt.hessenberg),
1059 ("tridiag", rt.tridiag),
1060 ("matvec", rt.matvec),
1061 ("bidiag_svd_dc", rt.bidiag_svd_dc),
1062 ("tridiag_evd_dc", rt.tridiag_evd_dc),
1063 ("triangular_inverse", rt.triangular_inverse),
1064 ("sparse_matmul", rt.sparse_matmul),
1065 ("householder_rank_one", rt.householder_rank_one),
1066 ("lu_swap", rt.lu_swap),
1067 ]
1068 }
1069
1070 #[test]
1071 fn every_class_resolves_to_its_table() {
1072 assert_eq!(Rates::for_class(HostClass::X86_64Avx512), &Rates::X86_64_AVX512);
1073 assert_eq!(Rates::for_class(HostClass::Aarch64Apple), &Rates::AARCH64_APPLE);
1074 for c in [HostClass::X86_64Avx2, HostClass::X86_64Scalar, HostClass::Aarch64, HostClass::Generic] {
1075 assert_eq!(Rates::for_class(c), &Rates::GENERIC, "{}", c.name());
1076 }
1077 assert_eq!(Rates::X86_64_AVX512.gemm_mac, Rate { real: 0.0641, cplx: 0.2565 });
1079 assert_eq!(Rates::X86_64_AVX512.gemm_single, Rate { real: 0.0320, cplx: 0.1283 });
1080 assert_eq!(Rates::AARCH64_APPLE.gemm_mac, Rate { real: 0.037, cplx: 0.165 });
1081 assert_eq!(Rates::AARCH64_APPLE.bidiag_svd_dc, Rate { real: 53.0, cplx: 53.0 });
1082 let g = Rates::X86_64_AVX512;
1084 assert!((g.gemm_single.real / (g.gemm_mac.real / 2.0) - 1.0).abs() < 0.01);
1085 assert!((g.gemm_single.cplx / (g.gemm_mac.cplx / 2.0) - 1.0).abs() < 0.01);
1086 }
1087
1088 #[test]
1089 fn generic_is_the_per_entry_minimum_of_the_measured_rows() {
1090 let (a, b, g) = (entries(&Rates::X86_64_AVX512), entries(&Rates::AARCH64_APPLE), entries(&Rates::GENERIC));
1091 for i in 0..a.len() {
1092 let (name, ga) = (a[i].0, a[i].1);
1093 assert_eq!(g[i].1.real, ga.real.min(b[i].1.real), "{name} real");
1094 assert_eq!(g[i].1.cplx, ga.cplx.min(b[i].1.cplx), "{name} cplx");
1095 assert!(g[i].1.real <= ga.real && g[i].1.real <= b[i].1.real, "{name}");
1096 }
1097 }
1098
1099 #[test]
1100 fn rows_are_positive() {
1101 for c in HostClass::ALL {
1102 for (name, r) in entries(Rates::for_class(c)) {
1103 assert!(r.real > 0.0 && r.cplx > 0.0, "{} {name}", c.name());
1104 }
1105 }
1106 }
1107
1108 #[test]
1109 fn detection_is_stable_and_the_host_row_is_its_class() {
1110 let c = HostClass::detect();
1111 for _ in 0..4 {
1112 assert_eq!(HostClass::detect(), c);
1113 }
1114 assert_eq!(HostClass::detect_uncached(), c);
1115 assert_eq!(rates(), Rates::for_class(c));
1116 assert_eq!(rate::bidiag(), rates().bidiag);
1117 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1118 if std::arch::is_x86_feature_detected!("avx512f") {
1119 assert_eq!(c, HostClass::X86_64Avx512);
1120 }
1121 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
1122 assert_eq!(c, HostClass::Aarch64Apple);
1123 }
1124
1125 #[test]
1126 fn outside_rows_follow_the_classes() {
1127 let us = |c, w: usize| outside_cost_of(c, nz(w)).as_nanos();
1128 assert_eq!((us(HostClass::X86_64Avx512, 2), us(HostClass::X86_64Avx512, 4), us(HostClass::X86_64Avx512, 6)), (9_700, 10_100, 8_700));
1129 assert_eq!((us(HostClass::Aarch64Apple, 2), us(HostClass::Aarch64Apple, 4), us(HostClass::Aarch64Apple, 6)), (14_000, 18_000, 19_000));
1130 assert_eq!(us(HostClass::Aarch64Apple, 3), 16_000);
1131 assert_eq!(us(HostClass::Aarch64Apple, 5), 18_500);
1132 for c in [HostClass::X86_64Avx2, HostClass::X86_64Scalar, HostClass::Aarch64, HostClass::Generic] {
1133 for w in [1, 2, 3, 6, 12] {
1134 assert_eq!(us(c, w), 40_000);
1135 }
1136 }
1137 assert_eq!(us(HostClass::Aarch64Apple, 12), 19_000);
1139 }
1140
1141 #[test]
1142 fn min_rayon_cost_is_the_cheapest_width() {
1143 for c in HostClass::ALL {
1144 let widths = |f: fn(HostClass, NonZeroUsize) -> Duration| (2..=12).map(|w| f(c, nz(w))).min().unwrap();
1145 assert_eq!(widths(outside_cost_of), Duration::from_nanos(outside_anchors(c).into_iter().min().unwrap()));
1146 assert_eq!(widths(in_pool_cost_of), Duration::from_nanos(in_pool_anchors(c).into_iter().min().unwrap()));
1147 }
1148 }
1149
1150 #[test]
1151 fn in_pool_rows_follow_the_classes() {
1152 let us = |c, w: usize| in_pool_cost_of(c, nz(w)).as_nanos();
1153 assert_eq!((us(HostClass::X86_64Avx512, 2), us(HostClass::X86_64Avx512, 4), us(HostClass::X86_64Avx512, 6)), (3_000, 3_600, 3_500));
1154 assert_eq!((us(HostClass::Aarch64Apple, 2), us(HostClass::Aarch64Apple, 4), us(HostClass::Aarch64Apple, 6)), (6_000, 11_000, 5_500));
1155 assert_eq!(us(HostClass::Generic, 8), 15_000);
1156 for c in HostClass::ALL {
1158 for w in 2..=12 {
1159 assert!(in_pool_cost_of(c, nz(w)) <= outside_cost_of(c, nz(w)), "{} width {w}", c.name());
1160 }
1161 }
1162 }
1163
1164 #[cfg(feature = "rayon")]
1168 #[test]
1169 fn a_held_spindle_group_is_a_worker_of_the_pool() {
1170 let s = ParShape::Rayon(nz(8));
1171 let outside = s.dispatch_cost(nz(8));
1172 assert!(!caller_in_pool());
1173 let held = spindle::with_lock(8, || (caller_in_pool(), s.dispatch_cost(nz(8)), s.min_dispatch_cost()));
1174 assert!(held.0);
1175 assert!(held.1 <= outside && held.2 <= s.min_dispatch_cost());
1176 if HostClass::detect() == HostClass::X86_64Avx512 {
1177 assert!(held.1 < outside);
1178 }
1179 }
1180
1181 #[cfg(feature = "threadteam")]
1184 #[test]
1185 fn host_class_and_outside_rows_agree_with_threadteam() {
1186 use threadteam::{DispatchCosts, DispatchKind};
1187 let tt = |c: HostClass| match c {
1188 HostClass::X86_64Avx512 => threadteam::HostClass::X86_64Avx512,
1189 HostClass::X86_64Avx2 => threadteam::HostClass::X86_64Avx2,
1190 HostClass::X86_64Scalar => threadteam::HostClass::X86_64Scalar,
1191 HostClass::Aarch64Apple => threadteam::HostClass::Aarch64Apple,
1192 HostClass::Aarch64 => threadteam::HostClass::Aarch64,
1193 HostClass::Generic => threadteam::HostClass::Generic,
1194 };
1195 assert_eq!(tt(HostClass::detect()), threadteam::HostClass::detect());
1196 for c in HostClass::ALL {
1197 assert_eq!(c.name(), tt(c).name());
1198 let costs = DispatchCosts::for_class(tt(c));
1199 for w in 2..=12 {
1200 assert_eq!(outside_cost_of(c, nz(w)), costs.cost(DispatchKind::Outside, nz(w)), "{} width {w}", c.name());
1201 assert_eq!(in_pool_cost_of(c, nz(w)), costs.cost(DispatchKind::InPool, nz(w)), "{} width {w}", c.name());
1202 }
1203 }
1204 }
1205
1206 #[test]
1207 fn fixed_and_never_keep_their_meaning() {
1208 let s = ParShape::Rayon(nz(6));
1209 let r = rate::lu_full_pivot();
1210 assert!(!ParThreshold::Never.region_pays::<f64>(s, usize::MAX, r, 1));
1211 assert!(ParThreshold::Fixed(100).region_pays::<f64>(ParShape::Seq, 100, r, 1));
1212 assert!(!ParThreshold::Fixed(100).region_pays::<f64>(s, 99, r, 1));
1213 assert!(ParThreshold::Fixed(100).region_pays::<f64>(s, 100, r, 1));
1214 assert_eq!(ParThreshold::from(0), ParThreshold::Derived);
1216 assert_eq!(ParThreshold::from(usize::MAX), ParThreshold::Never);
1217 assert_eq!(ParThreshold::from(7), ParThreshold::Fixed(7));
1218 assert_eq!(usize::from(ParThreshold::Fixed(0)), 1);
1219 }
1220
1221 #[test]
1222 fn derived_gates_never_pay_on_a_sequential_shape() {
1223 for (_, r, sections) in gates(rates()) {
1224 assert!(!ParThreshold::Derived.region_pays::<f64>(
1225 ParShape::Seq,
1226 usize::MAX / 2,
1227 r,
1228 sections
1229 ));
1230 assert!(!ParThreshold::Derived.region_pays::<c64>(
1231 ParShape::Seq,
1232 usize::MAX / 2,
1233 r,
1234 sections
1235 ));
1236 }
1237 }
1238
1239 #[cfg(feature = "threadteam")]
1240 #[test]
1241 fn derived_floors_on_a_team_follow_the_rule() {
1242 use threadteam::DispatchCosts;
1243 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1244 let d = team.dispatch_cost(nz(6)).as_secs_f64() * 1e9;
1245 for (name, r, sections) in gates(M5) {
1246 for (ty, ns) in [("f64", r.real), ("c64", r.cplx)] {
1247 let floor = if ty == "f64" {
1248 floor_units::<f64>(team, r, sections)
1249 } else {
1250 floor_units::<c64>(team, r, sections)
1251 };
1252 let want = PAYS * f64::from(sections) * d / ns;
1254 assert!(
1255 (floor as f64 - want).abs() <= 4.0 + want * 2e-3,
1256 "{name} {ty}: floor {floor}, rule {want}"
1257 );
1258 }
1259 }
1260 let pays = |u: usize, r: Rate, f: u32| {
1262 ParThreshold::Derived.region_pays::<f64>(team, u, r, f)
1263 };
1264 assert!(!pays(100 * 100, M5.lu_full_pivot, 1));
1266 assert!(pays(200 * 200, M5.lu_full_pivot, 1));
1267 assert!(floor_units::<f64>(team, M5.bidiag, 2) >= 2 * floor_units::<f64>(team, M5.bidiag, 1) - 2);
1269 assert!(pays(256 * 256, M5.matvec, 1));
1271 assert!(!pays(64 * 64, M5.matvec, 1));
1272 let lu = floor_units::<f64>(team, M5.lu_full_pivot, 1);
1275 let bidiag = floor_units::<f64>(team, M5.bidiag, 1);
1276 assert!(lu > bidiag);
1277 }
1278
1279 #[cfg(all(feature = "rayon", target_os = "macos", target_arch = "aarch64"))]
1280 #[test]
1281 fn derived_floors_outside_the_pool_are_larger_than_on_a_team() {
1282 let rayon = ParShape::Rayon(nz(6));
1283 for (name, r, sections) in gates(M5) {
1284 let outside = floor_units::<f64>(rayon, r, sections);
1285 #[cfg(feature = "threadteam")]
1286 {
1287 use threadteam::DispatchCosts;
1288 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1289 let t = floor_units::<f64>(team, r, sections);
1290 assert!(outside > 8 * t, "{name}: outside {outside}, team {t}");
1291 }
1292 let want = PAYS * f64::from(sections) * 19_000.0 / r.real;
1294 assert!((outside as f64 - want).abs() <= 4.0 + want * 2e-3, "{name}");
1295 }
1296 assert!(floor_units::<f64>(rayon, M5.bidiag, 2) > 192 * 256);
1299 }
1300
1301 #[cfg(feature = "rayon")]
1302 #[test]
1303 fn par_pays_reads_the_degree_of_the_token() {
1304 let seq = crate::Par::Seq;
1305 assert!(!par_pays::<f64>(&seq, usize::MAX / 2, rate::matvec(), 1));
1306 let par = crate::Par::rayon(6);
1307 assert!(par_pays::<f64>(&par, 10_000_000, rate::matvec(), 1));
1308 assert!(!par_pays::<f64>(&par, 1_000, rate::matvec(), 1));
1309 let one = crate::Par::rayon(1);
1310 assert!(!par_pays::<f64>(&one, 10_000_000, rate::matvec(), 1));
1311 }
1312
1313 #[test]
1314 fn rates_are_per_dtype() {
1315 let r = rate::bidiag();
1316 assert_eq!(r.of::<f64>(), r.real);
1317 assert_eq!(r.of::<c64>(), r.cplx);
1318 assert_eq!(r.of::<f32>(), r.real / 2.0);
1319 assert_eq!(r.of::<crate::c32>(), r.cplx / 2.0);
1320 assert_eq!(rate::gemm_mac().of::<f64>(), ns_per_mac::f64());
1321 assert_eq!(rate::gemm_mac().of::<c64>(), ns_per_mac::c64());
1322 }
1323
1324 #[cfg(feature = "rayon")]
1328 mod forced_parallel {
1329 use super::*;
1330 use crate::dyn_stack::{MemBuffer, MemStack};
1331 use crate::internal_prelude::*;
1332 use crate::linalg::cholesky::lblt::factor as bk;
1333 use crate::linalg::evd::{hessenberg, tridiag};
1334 use crate::linalg::lu::full_pivoting::factor as fplu;
1335 use crate::linalg::qr::col_pivoting::factor as cpqr;
1336 use crate::linalg::svd::bidiag;
1337 use crate::{Auto, Mat, MatRef, Par};
1338
1339 const N: usize = 96;
1340
1341 fn fill<T: crate::ComplexField>(n: usize, seed: usize) -> Mat<T> {
1342 Mat::from_fn(n, n, |i, j| {
1343 let h = (i * 2654435761usize)
1344 .wrapping_add(j * 40503)
1345 .wrapping_add(seed * 977)
1346 % 1009;
1347 from_f64::<T>(h as f64 / 504.0 - 1.0)
1348 })
1349 }
1350 fn close<T: crate::ComplexField<Real = f64>>(a: MatRef<'_, T>, b: MatRef<'_, T>) {
1351 let mut err = 0.0f64;
1352 for j in 0..a.ncols() {
1353 for i in 0..a.nrows() {
1354 err = err.max((a[(i, j)].clone() - b[(i, j)].clone()).abs());
1355 }
1356 }
1357 core::assert!(err <= 1e-9, "error {err}");
1358 }
1359
1360 fn check<T: crate::ComplexField<Real = f64>>() {
1361 let a0 = fill::<T>(N, 1);
1362 let sym = &a0 + a0.adjoint();
1363 let lu = |par: Par| {
1365 let mut a = a0.clone();
1366 let mut p = vec![0usize; 4 * N];
1367 let (r, rest) = p.split_at_mut(N);
1368 let (ri, rest) = rest.split_at_mut(N);
1369 let (c, ci) = rest.split_at_mut(N);
1370 let mut params: fplu::FullPivLuParams = Auto::<T>::auto();
1371 params.par_threshold = ParThreshold::Fixed(1);
1372 let mut mem = MemBuffer::new(fplu::lu_in_place_scratch::<usize, T>(
1373 N,
1374 N,
1375 par.shape(),
1376 params.into(),
1377 ));
1378 fplu::lu_in_place(
1379 a.as_mut(), r, ri, c, ci, par, MemStack::new(&mut mem), params.into(),
1380 );
1381 a
1382 };
1383 close(lu(Par::Seq).as_ref(), lu(Par::rayon(4)).as_ref());
1384 let bunch = |par: Par| {
1385 let mut a = sym.clone();
1386 let mut perm = vec![0usize; N];
1387 let mut perm_inv = vec![0usize; N];
1388 let mut sub = Mat::<T>::zeros(N, 1);
1389 let mut params: bk::LbltParams = Auto::<T>::auto();
1390 params.pivoting = bk::PivotingStrategy::Full;
1391 params.par_threshold = ParThreshold::Fixed(1);
1392 let mut mem = MemBuffer::new(bk::cholesky_in_place_scratch::<usize, T>(
1393 N,
1394 par.shape(),
1395 params.into(),
1396 ));
1397 bk::cholesky_in_place(
1398 a.as_mut(),
1399 sub.as_mut().col_mut(0).as_diagonal_mut(),
1400 &mut perm,
1401 &mut perm_inv,
1402 par,
1403 MemStack::new(&mut mem),
1404 params.into(),
1405 );
1406 a
1407 };
1408 close(bunch(Par::Seq).as_ref(), bunch(Par::rayon(4)).as_ref());
1409 let qr = |par: Par| {
1410 let mut a = a0.clone();
1411 let mut h = Mat::<T>::zeros(16, N);
1412 let mut p = vec![0usize; N];
1413 let mut pi = vec![0usize; N];
1414 let mut params: cpqr::ColPivQrParams = Auto::<T>::auto();
1415 params.par_threshold = ParThreshold::Fixed(1);
1416 let mut mem = MemBuffer::new(cpqr::qr_in_place_scratch::<usize, T>(
1417 N,
1418 N,
1419 16,
1420 par.shape(),
1421 params.into(),
1422 ));
1423 cpqr::qr_in_place(
1424 a.as_mut(), h.as_mut(), &mut p, &mut pi, par, MemStack::new(&mut mem), params.into(),
1425 );
1426 a
1427 };
1428 close(qr(Par::Seq).as_ref(), qr(Par::rayon(4)).as_ref());
1429 let bid = |par: Par| {
1430 let mut a = a0.clone();
1431 let mut hl = Mat::<T>::zeros(8, N);
1432 let mut hr = Mat::<T>::zeros(8, N - 1);
1433 let mut params: bidiag::BidiagParams = Auto::<T>::auto();
1434 params.par_threshold = ParThreshold::Fixed(1);
1435 let mut mem = MemBuffer::new(bidiag::bidiag_in_place_scratch::<T>(
1436 N,
1437 N,
1438 par.shape(),
1439 params.into(),
1440 ));
1441 bidiag::bidiag_in_place(
1442 a.as_mut(), hl.as_mut(), hr.as_mut(), par, MemStack::new(&mut mem), params.into(),
1443 );
1444 a
1445 };
1446 close(bid(Par::Seq).as_ref(), bid(Par::rayon(4)).as_ref());
1447 let hess = |par: Par| {
1448 let mut a = a0.clone();
1449 let mut h = Mat::<T>::zeros(8, N - 1);
1450 let mut params: hessenberg::HessenbergParams = Auto::<T>::auto();
1451 params.par_threshold = ParThreshold::Fixed(1);
1452 params.blocking_threshold = usize::MAX;
1453 let mut mem = MemBuffer::new(hessenberg::hessenberg_in_place_scratch::<T>(
1454 N,
1455 8,
1456 par.shape(),
1457 params.into(),
1458 ));
1459 hessenberg::hessenberg_in_place(
1460 a.as_mut(), h.as_mut(), par, MemStack::new(&mut mem), params.into(),
1461 );
1462 a
1463 };
1464 close(hess(Par::Seq).as_ref(), hess(Par::rayon(4)).as_ref());
1465 let tri = |par: Par| {
1466 let mut a = sym.clone();
1467 let mut h = Mat::<T>::zeros(8, N - 1);
1468 let mut params: tridiag::TridiagParams = Auto::<T>::auto();
1469 params.par_threshold = ParThreshold::Fixed(1);
1470 let mut mem = MemBuffer::new(tridiag::tridiag_in_place_scratch::<T>(
1471 N,
1472 par.shape(),
1473 params.into(),
1474 ));
1475 tridiag::tridiag_in_place(
1476 a.as_mut(), h.as_mut(), par, MemStack::new(&mut mem), params.into(),
1477 );
1478 a
1479 };
1480 close(tri(Par::Seq).as_ref(), tri(Par::rayon(4)).as_ref());
1481 }
1482
1483 #[test]
1484 fn forced_parallel_steps_equal_serial_f64() {
1485 check::<f64>();
1486 }
1487
1488 #[test]
1489 fn forced_parallel_steps_equal_serial_c64() {
1490 check::<c64>();
1491 }
1492 }
1493
1494 #[test]
1496 fn halves_join_gates_follow_the_rule() {
1497 let seq = crate::Par::Seq;
1498 for r in [rate::bidiag_svd_dc(), rate::tridiag_evd_dc(), rate::triangular_inverse()] {
1499 assert!(!halves_pays::<f64>(&seq, 1 << 20, r, 2));
1500 }
1501 #[cfg(feature = "rayon")]
1502 {
1503 let par = crate::Par::rayon(6);
1504 for r in [rate::bidiag_svd_dc(), rate::tridiag_evd_dc(), rate::triangular_inverse()] {
1505 let power = if r == rate::triangular_inverse() { 3 } else { 2 };
1507 let first = (2..4096).find(|&n| halves_pays::<f64>(&par, n, r, power)).expect("never pays");
1508 assert!(first > 8, "{first}");
1509 assert!(!halves_pays::<f64>(&par, first - 1, r, power));
1510 assert!(halves_pays::<f64>(&par, first, r, power));
1511 assert!(halves_pays::<f64>(&par, 2 * first, r, power));
1512 let h = first / 2;
1514 let work_ns = 2.0 * (h as f64).powi(power as i32) * r.real;
1515 let want = PAYS * par.shape().dispatch_cost(nz(2)).as_secs_f64() * 1e9;
1516 assert!(work_ns >= want * 0.999, "{work_ns} {want}");
1517 }
1518 }
1519 }
1520
1521 #[cfg(feature = "threadteam")]
1528 #[test]
1529 fn block_apply_split_starts_where_the_estimate_pays() {
1530 use threadteam::DispatchCosts;
1531 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1532 let pays = |m: usize| {
1533 let (n, k) = (8, m - 8);
1534 pays_at(block_apply_estimate_with::<f64>(M5, m, n, k, 2), 1, team.dispatch_cost(nz(6)))
1535 };
1536 assert!(!pays(72));
1537 assert!(pays(80));
1538 let gemm_only = |m: usize| {
1540 pays_at(serial_time(2 * m * 8 * (m - 8), M5.gemm_mac.real), 1, team.dispatch_cost(nz(6)))
1541 };
1542 assert!(!gemm_only(88) && gemm_only(96));
1543 let c = block_apply_estimate_with::<c64>(M5, 100, 8, 92, 2).as_secs_f64() * 1e9;
1545 let want = 2.0 * 100.0 * 8.0 * 92.0 * M5.block_apply_mac.cplx + 100.0 * 92.0 * M5.householder_rank_one.cplx;
1546 assert!((c - want).abs() < 2.0, "{c} {want}");
1547 }
1548
1549 #[cfg(feature = "threadteam")]
1550 #[test]
1551 fn join_gates_on_a_team_follow_the_width_two_cost() {
1552 use threadteam::DispatchCosts;
1553 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1554 let rayon = ParShape::Rayon(nz(6));
1555 for (name, r, sections) in gates(M5) {
1556 let t = floor_units::<f64>(team, r, sections);
1557 let o = floor_units::<f64>(rayon, r, sections);
1558 assert!(t < o, "{name}: team {t}, outside {o}");
1559 }
1560 let d2 = team.dispatch_cost(nz(2));
1562 assert_eq!(d2, Duration::from_micros(3));
1563 let par = |ns: f64| pays_at(Duration::from_secs_f64(ns * 1e-9), 1, d2);
1564 assert!(par(9000.0) && !par(8900.0));
1565 for (r, power, yes, no) in [
1569 (M5.bidiag_svd_dc, 2u32, 128usize, 8usize),
1570 (M5.tridiag_evd_dc, 2, 64, 8),
1571 (M5.triangular_inverse, 3, 256, 64),
1572 ] {
1573 let pays = |n: usize| {
1574 let h = n / 2;
1575 pays_at(
1576 serial_time(2 * h.pow(power), r.real),
1577 1,
1578 d2,
1579 )
1580 };
1581 assert!(pays(yes));
1582 assert!(!pays(no));
1583 }
1584 assert!(ParThreshold::Derived.region_pays::<f64>(team, 1_000_000, M5.sparse_matmul, 1));
1586 assert!(!ParThreshold::Derived.region_pays::<f64>(team, 1_000, M5.sparse_matmul, 1));
1587 }
1588}
1589
1590#[cfg(feature = "par-trace")]
1591#[doc(hidden)]
1592pub fn trace_pub(
1593 units: usize,
1594 sections: u32,
1595 estimate: Duration,
1596 dispatch: Duration,
1597 r: bool,
1598 loc: &'static core::panic::Location<'static>,
1599) {
1600 trace::record(loc, units, sections, estimate.as_secs_f64() * 1e9, dispatch.as_secs_f64() * 1e9, r);
1601}
1602
1603#[cfg(all(test, feature = "rayon", feature = "sparse-linalg"))]
1604mod join_results {
1605 use crate::dyn_stack::{MemBuffer, MemStack};
1608 use crate::linalg::evd::{self, ComputeEigenvectors};
1609 use crate::linalg::svd;
1610 use crate::linalg::triangular_inverse::{invert_lower_triangular, invert_unit_lower_triangular};
1611 use crate::{Accum, Mat, Par, Spec};
1612
1613 struct Rng(u64);
1614 impl Rng {
1615 fn next(&mut self) -> f64 {
1616 self.0 ^= self.0 << 13;
1617 self.0 ^= self.0 >> 7;
1618 self.0 ^= self.0 << 17;
1619 (self.0 >> 11) as f64 / (1u64 << 53) as f64 - 0.5
1620 }
1621 }
1622
1623 fn close(a: &[f64], b: &[f64], tol: f64) {
1624 let scale = a.iter().fold(1.0f64, |m, x| m.max(x.abs()));
1625 for (x, y) in a.iter().zip(b) {
1626 assert!((x - y).abs() <= tol * scale, "{x} {y}");
1627 }
1628 }
1629
1630 #[test]
1631 fn triangular_inverse_agrees_with_serial() {
1632 let n = 512;
1633 let mut r = Rng(3);
1634 let mut src = Mat::<f64>::from_fn(n, n, |_, _| r.next() * 0.1);
1635 for i in 0..n {
1636 src[(i, i)] = 2.0 + src[(i, i)].abs();
1637 }
1638 for unit in [false, true] {
1639 let mut a = Mat::<f64>::zeros(n, n);
1640 let mut b = Mat::<f64>::zeros(n, n);
1641 for (dst, par) in [(&mut a, Par::Seq), (&mut b, Par::rayon(4))] {
1642 if unit {
1643 invert_unit_lower_triangular(dst.as_mut(), src.as_ref(), par);
1644 } else {
1645 invert_lower_triangular(dst.as_mut(), src.as_ref(), par);
1646 }
1647 }
1648 for j in 0..n {
1649 close(
1650 &(0..n).map(|i| a[(i, j)]).collect::<Vec<_>>(),
1651 &(0..n).map(|i| b[(i, j)]).collect::<Vec<_>>(),
1652 1e-9,
1653 );
1654 }
1655 }
1656 }
1657
1658 #[test]
1659 fn bidiagonal_svd_agrees_with_serial() {
1660 let n = 512;
1661 let mut r = Rng(5);
1662 let d0: Vec<f64> = (0..n).map(|_| r.next() + 1.0).collect();
1663 let e: Vec<f64> = (0..n - 1).map(|_| r.next()).collect();
1664 let mut out = vec![];
1665 for par in [Par::Seq, Par::rayon(4)] {
1666 let mut d = d0.clone();
1667 let mut u = Mat::<f64>::zeros(n, n);
1668 let mut v = Mat::<f64>::zeros(n, n);
1669 let mut mem = MemBuffer::new(svd::bidiagonal_svd_scratch::<f64>(
1670 n,
1671 par.shape(),
1672 Spec::default(),
1673 ));
1674 svd::bidiagonal_svd(
1675 &mut d,
1676 &e,
1677 true,
1678 u.as_mut(),
1679 v.as_mut(),
1680 par,
1681 MemStack::new(&mut mem),
1682 Spec::default(),
1683 )
1684 .unwrap();
1685 out.push(d);
1686 }
1687 close(&out[0], &out[1], 1e-10);
1688 }
1689
1690 #[test]
1691 fn tridiagonal_evd_agrees_with_serial() {
1692 let n = 512;
1693 let mut r = Rng(6);
1694 let d0: Vec<f64> = (0..n).map(|_| r.next()).collect();
1695 let mut e0: Vec<f64> = (0..n - 1).map(|_| r.next()).collect();
1696 e0.push(0.0);
1697 let mut out = vec![];
1698 for par in [Par::Seq, Par::rayon(4)] {
1699 let mut u = Mat::<f64>::zeros(n, n);
1700 let mut s = Mat::<f64>::zeros(n, 1);
1701 let mut mem = MemBuffer::new(evd::tridiagonal_self_adjoint_evd_scratch::<f64>(
1702 n,
1703 ComputeEigenvectors::Yes,
1704 par.shape(),
1705 Spec::default(),
1706 ));
1707 let d = crate::col::ColRef::from_slice(&d0);
1708 let e = crate::col::ColRef::from_slice(&e0);
1709 evd::tridiagonal_self_adjoint_evd(
1710 d.as_diagonal(),
1711 e.as_diagonal(),
1712 s.as_mut().col_mut(0).as_diagonal_mut(),
1713 Some(u.as_mut()),
1714 par,
1715 MemStack::new(&mut mem),
1716 Spec::default(),
1717 )
1718 .unwrap();
1719 out.push((0..n).map(|i| s[(i, 0)]).collect::<Vec<_>>());
1720 }
1721 close(&out[0], &out[1], 1e-10);
1722 }
1723
1724 #[test]
1725 fn sparse_matmul_agrees_with_serial() {
1726 use crate::sparse::SparseColMat;
1727 use crate::sparse::Triplet;
1728 use crate::sparse::linalg::matmul::sparse_sparse_matmul;
1729 let n = 6000;
1730 let mk = |seed: u64| {
1731 let mut r = Rng(seed);
1732 let mut t = vec![];
1733 for j in 0..n {
1734 for _ in 0..24 {
1735 let i = ((r.next() + 0.5) * n as f64) as usize % n;
1736 t.push(Triplet::new(i, j, r.next()));
1737 }
1738 }
1739 SparseColMat::<usize, f64>::try_new_from_triplets(n, n, &t).unwrap()
1740 };
1741 let (a, b) = (mk(1), mk(2));
1742 let s = sparse_sparse_matmul(a.as_ref(), b.as_ref(), 1.0, Par::Seq).unwrap();
1743 let p = sparse_sparse_matmul(a.as_ref(), b.as_ref(), 1.0, Par::rayon(4)).unwrap();
1744 close(s.val(), p.val(), 1e-12);
1745 let _ = Accum::Add;
1746 }
1747}