1use crate::ParShape;
11use core::num::NonZeroUsize;
12use core::time::Duration;
13
14#[cfg(feature = "threadteam")]
15pub use threadteam::{PAYS, pays_at};
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)]
43pub enum HostClass {
44 X86_64Avx512,
46 X86_64Avx2,
48 X86_64Scalar,
50 Aarch64Apple,
52 Aarch64,
54 Generic,
56}
57
58impl HostClass {
59 pub const ALL: [HostClass; 6] = [
61 HostClass::X86_64Avx512,
62 HostClass::X86_64Avx2,
63 HostClass::X86_64Scalar,
64 HostClass::Aarch64Apple,
65 HostClass::Aarch64,
66 HostClass::Generic,
67 ];
68
69 pub fn detect() -> Self {
72 use core::sync::atomic::{AtomicU8, Ordering};
73 static CLASS: AtomicU8 = AtomicU8::new(0);
74 let c = CLASS.load(Ordering::Relaxed);
75 if c != 0 {
76 return Self::ALL[c as usize - 1];
77 }
78 let class = Self::detect_uncached();
79 let idx = Self::ALL.iter().position(|&k| k == class).unwrap();
80 CLASS.store(idx as u8 + 1, Ordering::Relaxed);
81 class
82 }
83
84 #[cfg(all(target_arch = "x86_64", feature = "std"))]
85 fn detect_uncached() -> Self {
86 if std::arch::is_x86_feature_detected!("avx512f") {
87 HostClass::X86_64Avx512
88 } else if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
89 HostClass::X86_64Avx2
90 } else {
91 HostClass::X86_64Scalar
92 }
93 }
94
95 #[cfg(all(target_arch = "x86_64", not(feature = "std")))]
96 fn detect_uncached() -> Self {
97 if cfg!(target_feature = "avx512f") {
98 HostClass::X86_64Avx512
99 } else if cfg!(all(target_feature = "avx2", target_feature = "fma")) {
100 HostClass::X86_64Avx2
101 } else {
102 HostClass::X86_64Scalar
103 }
104 }
105
106 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
107 fn detect_uncached() -> Self {
108 HostClass::Aarch64Apple
109 }
110
111 #[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
112 fn detect_uncached() -> Self {
113 HostClass::Aarch64
114 }
115
116 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
117 fn detect_uncached() -> Self {
118 HostClass::Generic
119 }
120
121 pub const fn name(self) -> &'static str {
123 match self {
124 HostClass::X86_64Avx512 => "x86_64-avx512",
125 HostClass::X86_64Avx2 => "x86_64-avx2",
126 HostClass::X86_64Scalar => "x86_64-scalar",
127 HostClass::Aarch64Apple => "aarch64-apple",
128 HostClass::Aarch64 => "aarch64",
129 HostClass::Generic => "generic",
130 }
131 }
132}
133
134pub mod ns_per_mac {
137 use super::rates;
138 pub fn f64() -> f64 {
140 rates().gemm_mac.real
141 }
142 pub fn c64() -> f64 {
144 rates().gemm_mac.cplx
145 }
146 pub fn f32() -> f64 {
148 rates().gemm_single.real
149 }
150 pub fn c32() -> f64 {
152 rates().gemm_single.cplx
153 }
154}
155
156pub const NS_PER_SWAP: f64 = 0.55;
162
163#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
171pub enum ParThreshold {
172 #[default]
174 Derived,
175 Fixed(usize),
180 Never,
182}
183
184impl From<usize> for ParThreshold {
188 fn from(value: usize) -> Self {
189 match value {
190 0 => ParThreshold::Derived,
191 usize::MAX => ParThreshold::Never,
192 n => ParThreshold::Fixed(n),
193 }
194 }
195}
196
197impl From<ParThreshold> for usize {
200 fn from(value: ParThreshold) -> Self {
201 match value {
202 ParThreshold::Derived => 0,
203 ParThreshold::Fixed(n) => n.max(1),
204 ParThreshold::Never => usize::MAX,
205 }
206 }
207}
208
209impl ParThreshold {
210 #[cfg_attr(feature = "par-trace", track_caller)]
218 pub(crate) fn region_width<T: crate::ComplexField>(
219 self,
220 par: &crate::Par,
221 units: usize,
222 rate: Rate,
223 sections: u32,
224 ) -> usize {
225 match self {
226 Self::Never => 1,
227 Self::Fixed(floor) => {
228 if units >= floor {
229 par.degree()
230 } else {
231 1
232 }
233 },
234 Self::Derived => par_width::<T>(par, units, rate, sections),
235 }
236 }
237}
238
239#[derive(Copy, Clone, Debug, PartialEq)]
245pub struct Rate {
246 pub real: f64,
248 pub cplx: f64,
250}
251
252impl Rate {
253 pub fn of<T: crate::ComplexField>(self) -> f64 {
255 if const { T::IS_NATIVE_F32 } {
256 self.real / 2.0
257 } else if const { T::IS_NATIVE_C32 } {
258 self.cplx / 2.0
259 } else if const { T::IS_REAL } {
260 self.real
261 } else {
262 self.cplx
263 }
264 }
265}
266
267#[derive(Copy, Clone, Debug, PartialEq)]
289pub struct Rates {
290 pub gemm_mac: Rate,
293 pub gemm_single: Rate,
295 pub block_apply_mac: Rate,
297 pub lu_full_pivot: Rate,
300 pub bunch_kaufman: Rate,
303 pub qr_col_pivot: Rate,
306 pub bidiag: Rate,
309 pub hessenberg: Rate,
312 pub tridiag: Rate,
315 pub matvec: Rate,
318 pub bidiag_svd_dc: Rate,
325 pub tridiag_evd_dc: Rate,
329 pub triangular_inverse: Rate,
334 pub sparse_matmul: Rate,
337 pub householder_rank_one: Rate,
339 pub lu_swap: Rate,
343}
344
345const fn fmin(a: f64, b: f64) -> f64 {
346 if b < a { b } else { a }
347}
348
349const fn rmin(a: Rate, b: Rate) -> Rate {
350 Rate { real: fmin(a.real, b.real), cplx: fmin(a.cplx, b.cplx) }
351}
352
353const fn r(real: f64, cplx: f64) -> Rate {
354 Rate { real, cplx }
355}
356
357impl Rates {
358 const fn min_of(a: &Rates, b: &Rates) -> Rates {
360 Rates {
361 gemm_mac: rmin(a.gemm_mac, b.gemm_mac),
362 gemm_single: rmin(a.gemm_single, b.gemm_single),
363 block_apply_mac: rmin(a.block_apply_mac, b.block_apply_mac),
364 lu_full_pivot: rmin(a.lu_full_pivot, b.lu_full_pivot),
365 bunch_kaufman: rmin(a.bunch_kaufman, b.bunch_kaufman),
366 qr_col_pivot: rmin(a.qr_col_pivot, b.qr_col_pivot),
367 bidiag: rmin(a.bidiag, b.bidiag),
368 hessenberg: rmin(a.hessenberg, b.hessenberg),
369 tridiag: rmin(a.tridiag, b.tridiag),
370 matvec: rmin(a.matvec, b.matvec),
371 bidiag_svd_dc: rmin(a.bidiag_svd_dc, b.bidiag_svd_dc),
372 tridiag_evd_dc: rmin(a.tridiag_evd_dc, b.tridiag_evd_dc),
373 triangular_inverse: rmin(a.triangular_inverse, b.triangular_inverse),
374 sparse_matmul: rmin(a.sparse_matmul, b.sparse_matmul),
375 householder_rank_one: rmin(a.householder_rank_one, b.householder_rank_one),
376 lu_swap: rmin(a.lu_swap, b.lu_swap),
377 }
378 }
379
380 pub const X86_64_AVX512: Rates = Rates {
382 gemm_mac: r(0.0641, 0.2565),
383 gemm_single: r(0.0320, 0.1283),
384 block_apply_mac: r(0.0676, 0.2685),
385 lu_full_pivot: r(0.4011, 0.8679),
386 bunch_kaufman: r(0.8295, 1.3904),
387 qr_col_pivot: r(0.3645, 1.0320),
388 bidiag: r(1.0427, 2.1997),
389 hessenberg: r(0.8576, 2.2212),
390 tridiag: r(0.7695, 1.9252),
391 matvec: r(0.1654, 0.4215),
393 bidiag_svd_dc: r(152.9, 152.9),
394 tridiag_evd_dc: r(97.4, 97.4),
395 triangular_inverse: r(0.01936, 0.07228),
396 sparse_matmul: r(4.1315, 5.2342),
398 householder_rank_one: r(0.4783, 1.0303),
399 lu_swap: r(0.98, 1.30),
403 };
404
405 pub const AARCH64_APPLE: Rates = Rates {
407 gemm_mac: r(0.037, 0.165),
408 gemm_single: r(0.037 / 2.0, 0.165 / 2.0),
409 block_apply_mac: r(0.037, 0.165),
410 lu_full_pivot: r(0.216, 0.6),
411 bunch_kaufman: r(0.238, 0.38),
412 qr_col_pivot: r(0.202, 0.713),
413 bidiag: r(0.48, 1.36),
414 hessenberg: r(0.451, 1.57),
415 tridiag: r(0.339, 1.16),
416 matvec: r(0.1, 0.34),
417 bidiag_svd_dc: r(53.0, 53.0),
418 tridiag_evd_dc: r(31.0, 31.0),
419 triangular_inverse: r(0.0096, 0.0344),
420 sparse_matmul: r(1.5, 1.8),
421 householder_rank_one: r(0.212, 0.725),
422 lu_swap: r(NS_PER_SWAP, NS_PER_SWAP),
423 };
424
425 pub const GENERIC: Rates = Rates::min_of(&Rates::X86_64_AVX512, &Rates::AARCH64_APPLE);
427
428 pub fn for_class(class: HostClass) -> &'static Rates {
430 static AVX512: Rates = Rates::X86_64_AVX512;
431 static APPLE: Rates = Rates::AARCH64_APPLE;
432 static GENERIC: Rates = Rates::GENERIC;
433 match class {
434 HostClass::X86_64Avx512 => &AVX512,
435 HostClass::Aarch64Apple => &APPLE,
436 HostClass::X86_64Avx2 | HostClass::X86_64Scalar | HostClass::Aarch64 | HostClass::Generic => &GENERIC,
437 }
438 }
439}
440
441pub fn rates() -> &'static Rates {
443 Rates::for_class(HostClass::detect())
444}
445
446pub mod rate {
449 use super::{Rate, rates};
450 macro_rules! families {
451 ($($name:ident),* $(,)?) => {$(
452 #[doc = core::concat!("`Rates::", core::stringify!($name), "` of the host class.")]
453 pub fn $name() -> Rate { rates().$name }
454 )*};
455 }
456 families!(
457 gemm_mac,
458 block_apply_mac,
459 lu_full_pivot,
460 bunch_kaufman,
461 qr_col_pivot,
462 bidiag,
463 hessenberg,
464 tridiag,
465 matvec,
466 bidiag_svd_dc,
467 tridiag_evd_dc,
468 triangular_inverse,
469 sparse_matmul,
470 householder_rank_one,
471 lu_swap,
472 );
473}
474
475pub(crate) fn gemm_ns_per_mac<T: crate::ComplexField>() -> f64 {
479 if const { T::IS_NATIVE_F32 } {
480 ns_per_mac::f32()
481 } else if const { T::IS_NATIVE_C32 } {
482 ns_per_mac::c32()
483 } else if const { T::IS_REAL } {
484 ns_per_mac::f64()
485 } else {
486 ns_per_mac::c64()
487 }
488}
489
490
491#[cfg(feature = "par-trace")]
495pub mod trace {
496 use std::sync::Mutex;
497 use std::sync::atomic::{AtomicBool, Ordering};
498 #[derive(Clone, Debug)]
500 pub struct Record {
501 pub site: &'static str,
503 pub dims: [usize; 3],
505 pub units: usize,
507 pub sections: u32,
509 pub estimate_ns: f64,
511 pub dispatch_ns: f64,
513 pub width: usize,
515 pub parallel: bool,
517 }
518 pub static N_TASKS_MODE: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
521 static ON: AtomicBool = AtomicBool::new(false);
522 static LOG: Mutex<Vec<Record>> = Mutex::new(Vec::new());
523 std::thread_local! {
524 static DIMS: core::cell::Cell<(&'static str, [usize; 3])> = const { core::cell::Cell::new(("", [0; 3])) };
525 }
526 pub fn enable(on: bool) {
528 if on {
529 LOG.lock().unwrap().clear();
530 }
531 ON.store(on, Ordering::SeqCst);
532 }
533 pub fn take() -> Vec<Record> {
535 core::mem::take(&mut *LOG.lock().unwrap())
536 }
537 pub fn shape(label: &'static str, a: usize, b: usize, c: usize) {
539 DIMS.with(|d| d.set((label, [a, b, c])));
540 }
541 pub(crate) fn record(
542 loc: &'static core::panic::Location<'static>,
543 units: usize,
544 sections: u32,
545 estimate_ns: f64,
546 dispatch_ns: f64,
547 width: usize,
548 parallel: bool,
549 ) {
550 if !ON.load(Ordering::Relaxed) {
551 return;
552 }
553 let (label, dims) = DIMS.with(|d| d.replace(("", [0; 3])));
554 let site: &'static str = if label.is_empty() {
555 Box::leak(format!("{}:{}", loc.file().rsplit('/').next().unwrap(), loc.line()).into_boxed_str())
556 } else {
557 label
558 };
559 LOG.lock().unwrap().push(Record { site, dims, units, sections, estimate_ns, dispatch_ns, width, parallel });
560 }
561}
562
563pub(crate) fn par_pays_time(par: &crate::Par, estimate: Duration, sections: u32, width: usize) -> bool {
568 let width = width.min(par.degree());
569 let Some(width) = NonZeroUsize::new(width).filter(|w| w.get() >= 2) else {
570 return false;
571 };
572 match par {
573 crate::Par::Seq => false,
574 #[cfg(feature = "threadteam")]
575 crate::Par::Team(team) => {
576 pays_at(estimate, sections, team.dispatch_cost(width))
579 },
580 #[cfg(not(feature = "threadteam"))]
582 _ => false,
583 }
584}
585
586pub(crate) fn par_can_pay_time(par: &crate::Par, estimate: Duration, sections: u32) -> bool {
591 if par.degree() < 2 {
592 return false;
593 }
594 match par {
595 crate::Par::Seq => false,
596 #[cfg(feature = "threadteam")]
597 crate::Par::Team(team) => par_can_pay_time_team(team, estimate, sections),
598 #[cfg(not(feature = "threadteam"))]
599 _ => false,
600 }
601}
602
603#[cfg(feature = "threadteam")]
605pub(crate) fn par_can_pay_time_team(team: &threadteam::TeamExec<'_>, estimate: Duration, sections: u32) -> bool {
606 team.size().get() >= 2
607 && pays_at(estimate, sections, team.dispatch_costs().min_cost(threadteam::DispatchKind::Team))
608}
609
610pub(crate) fn par_width_time(par: &crate::Par, estimate: Duration, sections: u32) -> usize {
618 par_width_time_upto(par, estimate, sections, usize::MAX)
619}
620
621pub(crate) fn par_width_time_upto(par: &crate::Par, estimate: Duration, sections: u32, max: usize) -> usize {
624 match par {
625 #[cfg(feature = "threadteam")]
626 crate::Par::Team(team) => par_width_team(team, estimate, sections, max),
627 _ => {
628 let _ = (estimate, sections, max);
629 1
630 },
631 }
632}
633
634#[cfg(feature = "threadteam")]
636pub(crate) fn par_width_team(team: &threadteam::TeamExec<'_>, estimate: Duration, sections: u32, max: usize) -> usize {
637 if max < 2 || !par_can_pay_time_team(team, estimate, sections) {
640 return 1;
641 }
642 (2..=team.size().get().min(max))
643 .rev()
644 .find(|&w| pays_at(estimate, sections, team.dispatch_cost(NonZeroUsize::new(w).unwrap())))
645 .unwrap_or(1)
646}
647
648#[cfg_attr(feature = "par-trace", track_caller)]
651pub(crate) fn par_width<T: crate::ComplexField>(
652 par: &crate::Par,
653 units: usize,
654 rate: Rate,
655 sections: u32,
656) -> usize {
657 par_width_upto::<T>(par, units, rate, sections, usize::MAX)
658}
659
660#[cfg_attr(feature = "par-trace", track_caller)]
662pub(crate) fn par_width_upto<T: crate::ComplexField>(
663 par: &crate::Par,
664 units: usize,
665 rate: Rate,
666 sections: u32,
667 max: usize,
668) -> usize {
669 if par.degree() < 2 || max < 2 {
670 return 1;
671 }
672 let w = par_width_time_upto(par, serial_time(units, rate.of::<T>()), sections, max);
673 #[cfg(feature = "par-trace")]
674 trace_decision(par.shape(), w, units, rate.of::<T>(), sections, w > 1, core::panic::Location::caller());
675 w
676}
677
678#[cfg(feature = "par-trace")]
679fn trace_decision(
680 shape: ParShape,
681 width: usize,
682 units: usize,
683 ns: f64,
684 sections: u32,
685 r: bool,
686 loc: &'static core::panic::Location<'static>,
687) {
688 let d = NonZeroUsize::new(width)
689 .map(|w| shape.dispatch_cost(w).as_secs_f64() * 1e9)
690 .unwrap_or(f64::INFINITY);
691 trace::record(loc, units, sections, units as f64 * ns, d, width, r);
692}
693
694#[cfg_attr(feature = "par-trace", track_caller)]
699pub(crate) fn join_pays<T: crate::ComplexField>(
700 par: &crate::Par,
701 units: usize,
702 rate: Rate,
703 sections: u32,
704) -> bool {
705 if par.degree() < 2 {
706 return false;
707 }
708 let r = par_pays_time(par, serial_time(units, rate.of::<T>()), sections, 2);
709 #[cfg(feature = "par-trace")]
710 trace_decision(par.shape(), 2, units, rate.of::<T>(), sections, r, core::panic::Location::caller());
711 r
712}
713
714pub(crate) fn halves_pays<T: crate::ComplexField>(
722 par: &crate::Par,
723 n: usize,
724 rate: Rate,
725 power: u32,
726) -> bool {
727 let h = n / 2;
728 join_pays::<T>(par, h.saturating_pow(power).saturating_mul(2), rate, 1)
729}
730
731pub(crate) fn block_apply_estimate<T: crate::ComplexField>(
738 m: usize,
739 n: usize,
740 k: usize,
741) -> Duration {
742 block_apply_estimate_with::<T>(rates(), m, n, k)
743}
744
745pub(crate) fn block_apply_estimate_with<T: crate::ComplexField>(
747 rt: &Rates,
748 m: usize,
749 n: usize,
750 k: usize,
751) -> Duration {
752 let macs = m.saturating_mul(n).saturating_mul(k).saturating_mul(2);
753 let ns = macs as f64 * rt.block_apply_mac.of::<T>() + m.saturating_mul(k) as f64 * rt.householder_rank_one.of::<T>();
755 Duration::from_secs_f64(ns * 1e-9)
756}
757
758pub(crate) fn serial_time(count: usize, ns: f64) -> Duration {
760 Duration::from_secs_f64(count as f64 * ns * 1e-9)
761}
762
763impl ParShape {
764 pub fn dispatch_cost(self, width: NonZeroUsize) -> Duration {
773 let width = width.min(NonZeroUsize::new(self.degree().max(1)).unwrap());
774 if width.get() < 2 {
775 return Duration::MAX;
776 }
777 match self {
778 Self::Seq => Duration::MAX,
779 #[cfg(feature = "threadteam")]
780 Self::Team(_, costs) => costs.cost(threadteam::DispatchKind::Team, width),
781 }
782 }
783
784 pub fn min_dispatch_cost(self) -> Duration {
788 if self.degree() < 2 {
789 return Duration::MAX;
790 }
791 match self {
792 Self::Seq => Duration::MAX,
793 #[cfg(feature = "threadteam")]
794 Self::Team(_, costs) => costs.min_cost(threadteam::DispatchKind::Team),
795 }
796 }
797
798 pub fn pays(self, estimate: Duration, sections: u32, width: NonZeroUsize) -> bool {
802 pays_at(estimate, sections, self.dispatch_cost(width))
803 }
804
805 pub fn pays_full(self, estimate: Duration, sections: u32) -> bool {
807 match NonZeroUsize::new(self.degree()) {
808 Some(width) => self.pays(estimate, sections, width),
809 None => false,
810 }
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use crate::c64;
818
819 fn nz(n: usize) -> NonZeroUsize {
820 NonZeroUsize::new(n).unwrap()
821 }
822
823 #[cfg(feature = "rayon")]
824 #[test]
825 fn seq_never_pays() {
826 assert_eq!(ParShape::Seq.dispatch_cost(nz(4)), Duration::MAX);
827 assert!(!ParShape::Seq.pays(Duration::from_secs(10), 1, nz(4)));
828 assert!(!ParShape::Seq.pays_full(Duration::from_secs(10), 1));
829 }
830
831 #[cfg(feature = "rayon")]
832 #[test]
833 fn min_dispatch_cost_is_the_cheapest_width() {
834 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
835 let us = Duration::from_micros;
836 for t in [[us(9), us(5), us(3)], [us(4), us(4), us(4)], [us(2), us(7), us(12)]] {
838 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic).with(DispatchKind::Team, WidthCosts::new(t));
839 let s = ParShape::Team(nz(6), costs);
840 let want = (2..=6).map(|w| s.dispatch_cost(nz(w))).min().unwrap();
841 assert_eq!(s.min_dispatch_cost(), want, "{t:?}");
842 }
843 assert_eq!(ParShape::Seq.min_dispatch_cost(), Duration::MAX);
844 assert_eq!(ParShape::Team(nz(1), DispatchCosts::for_class(threadteam::HostClass::Generic)).min_dispatch_cost(), Duration::MAX);
845 }
846
847 #[cfg(feature = "rayon")]
848 #[test]
849 fn pays_is_the_multiple_of_the_cost() {
850 let d = Duration::from_micros(2);
851 assert!(pays_at(Duration::from_micros(6), 1, d));
852 assert!(!pays_at(Duration::from_nanos(5999), 1, d));
853 assert!(!pays_at(Duration::from_micros(11), 2, d));
854 assert!(pays_at(Duration::from_micros(12), 2, d));
855 assert!(!pays_at(Duration::from_secs(1), 1, Duration::MAX));
856 }
857
858 #[test]
859 fn team_shape_carries_the_costs_of_its_token() {
860 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
861 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic)
862 .with(DispatchKind::Team, WidthCosts::flat(Duration::from_micros(5)));
863 let s = ParShape::Team(nz(4), costs);
864 assert_eq!(s.dispatch_cost(nz(4)), Duration::from_micros(5));
865 assert_eq!(s.dispatch_cost(nz(1)), Duration::MAX);
866 assert!(s.pays(Duration::from_micros(16), 1, nz(4)));
869 assert!(!s.pays(Duration::from_micros(14), 1, nz(4)));
870 }
871
872 #[test]
873 fn gemm_rates_are_per_dtype() {
874 assert_eq!(gemm_ns_per_mac::<f64>(), ns_per_mac::f64());
875 assert_eq!(gemm_ns_per_mac::<crate::c64>(), ns_per_mac::c64());
876 assert_eq!(gemm_ns_per_mac::<f32>(), ns_per_mac::f32());
877 assert_eq!(gemm_ns_per_mac::<crate::c32>(), ns_per_mac::c32());
878 assert!(ns_per_mac::c64() > 4.0 * ns_per_mac::f64());
880 }
881
882 fn floor_units<T: crate::ComplexField>(
884 shape: ParShape,
885 r: Rate,
886 sections: u32,
887 ) -> usize {
888 let pays = |u: usize| {
889 shape.pays_full(serial_time(u, r.of::<T>()), sections)
890 };
891 let mut hi = 1usize;
892 while !pays(hi) {
893 hi *= 2;
894 assert!(hi < 1 << 40, "never pays");
895 }
896 let mut lo = hi / 2;
897 while hi - lo > 1 {
898 let mid = (lo + hi) / 2;
899 if pays(mid) {
900 hi = mid;
901 } else {
902 lo = mid;
903 }
904 }
905 hi
906 }
907
908 fn gates(rt: &Rates) -> [(&'static str, Rate, u32); 10] {
910 [
911 ("lu_full_pivot", rt.lu_full_pivot, 1),
912 ("bunch_kaufman", rt.bunch_kaufman, 1),
913 ("qr_col_pivot", rt.qr_col_pivot, 1),
914 ("bidiag", rt.bidiag, 2),
915 ("hessenberg", rt.hessenberg, 2),
916 ("tridiag", rt.tridiag, 1),
917 ("matvec", rt.matvec, 1),
918 ("householder_rank_one", rt.householder_rank_one, 1),
919 ("gemm_mac", rt.gemm_mac, 1),
920 ("sparse_matmul", rt.sparse_matmul, 1),
921 ]
922 }
923
924 const M5: &Rates = &Rates::AARCH64_APPLE;
927
928 fn entries(rt: &Rates) -> [(&'static str, Rate); 16] {
930 [
931 ("gemm_mac", rt.gemm_mac),
932 ("gemm_single", rt.gemm_single),
933 ("block_apply_mac", rt.block_apply_mac),
934 ("lu_full_pivot", rt.lu_full_pivot),
935 ("bunch_kaufman", rt.bunch_kaufman),
936 ("qr_col_pivot", rt.qr_col_pivot),
937 ("bidiag", rt.bidiag),
938 ("hessenberg", rt.hessenberg),
939 ("tridiag", rt.tridiag),
940 ("matvec", rt.matvec),
941 ("bidiag_svd_dc", rt.bidiag_svd_dc),
942 ("tridiag_evd_dc", rt.tridiag_evd_dc),
943 ("triangular_inverse", rt.triangular_inverse),
944 ("sparse_matmul", rt.sparse_matmul),
945 ("householder_rank_one", rt.householder_rank_one),
946 ("lu_swap", rt.lu_swap),
947 ]
948 }
949
950 #[test]
951 fn every_class_resolves_to_its_table() {
952 assert_eq!(Rates::for_class(HostClass::X86_64Avx512), &Rates::X86_64_AVX512);
953 assert_eq!(Rates::for_class(HostClass::Aarch64Apple), &Rates::AARCH64_APPLE);
954 for c in [HostClass::X86_64Avx2, HostClass::X86_64Scalar, HostClass::Aarch64, HostClass::Generic] {
955 assert_eq!(Rates::for_class(c), &Rates::GENERIC, "{}", c.name());
956 }
957 assert_eq!(Rates::X86_64_AVX512.gemm_mac, Rate { real: 0.0641, cplx: 0.2565 });
959 assert_eq!(Rates::X86_64_AVX512.gemm_single, Rate { real: 0.0320, cplx: 0.1283 });
960 assert_eq!(Rates::AARCH64_APPLE.gemm_mac, Rate { real: 0.037, cplx: 0.165 });
961 assert_eq!(Rates::AARCH64_APPLE.bidiag_svd_dc, Rate { real: 53.0, cplx: 53.0 });
962 let g = Rates::X86_64_AVX512;
964 assert!((g.gemm_single.real / (g.gemm_mac.real / 2.0) - 1.0).abs() < 0.01);
965 assert!((g.gemm_single.cplx / (g.gemm_mac.cplx / 2.0) - 1.0).abs() < 0.01);
966 }
967
968 #[test]
969 fn generic_is_the_per_entry_minimum_of_the_measured_rows() {
970 let (a, b, g) = (entries(&Rates::X86_64_AVX512), entries(&Rates::AARCH64_APPLE), entries(&Rates::GENERIC));
971 for i in 0..a.len() {
972 let (name, ga) = (a[i].0, a[i].1);
973 assert_eq!(g[i].1.real, ga.real.min(b[i].1.real), "{name} real");
974 assert_eq!(g[i].1.cplx, ga.cplx.min(b[i].1.cplx), "{name} cplx");
975 assert!(g[i].1.real <= ga.real && g[i].1.real <= b[i].1.real, "{name}");
976 }
977 }
978
979 #[test]
980 fn rows_are_positive() {
981 for c in HostClass::ALL {
982 for (name, r) in entries(Rates::for_class(c)) {
983 assert!(r.real > 0.0 && r.cplx > 0.0, "{} {name}", c.name());
984 }
985 }
986 }
987
988 #[test]
989 fn detection_is_stable_and_the_host_row_is_its_class() {
990 let c = HostClass::detect();
991 for _ in 0..4 {
992 assert_eq!(HostClass::detect(), c);
993 }
994 assert_eq!(HostClass::detect_uncached(), c);
995 assert_eq!(rates(), Rates::for_class(c));
996 assert_eq!(rate::bidiag(), rates().bidiag);
997 #[cfg(all(target_arch = "x86_64", feature = "std"))]
998 if std::arch::is_x86_feature_detected!("avx512f") {
999 assert_eq!(c, HostClass::X86_64Avx512);
1000 }
1001 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
1002 assert_eq!(c, HostClass::Aarch64Apple);
1003 }
1004
1005 #[test]
1007 fn host_class_agrees_with_threadteam() {
1008 let tt = |c: HostClass| match c {
1009 HostClass::X86_64Avx512 => threadteam::HostClass::X86_64Avx512,
1010 HostClass::X86_64Avx2 => threadteam::HostClass::X86_64Avx2,
1011 HostClass::X86_64Scalar => threadteam::HostClass::X86_64Scalar,
1012 HostClass::Aarch64Apple => threadteam::HostClass::Aarch64Apple,
1013 HostClass::Aarch64 => threadteam::HostClass::Aarch64,
1014 HostClass::Generic => threadteam::HostClass::Generic,
1015 };
1016 assert_eq!(tt(HostClass::detect()), threadteam::HostClass::detect());
1017 for c in HostClass::ALL {
1018 assert_eq!(c.name(), tt(c).name());
1019 }
1020 }
1021
1022 #[test]
1026 fn the_team_cost_is_threadteams_table_at_every_width() {
1027 use threadteam::{DispatchCosts, DispatchKind, HostClass, WidthCosts};
1028 let us = Duration::from_micros;
1029 let rows = [
1030 DispatchCosts::for_class(HostClass::Generic),
1031 DispatchCosts::for_class(HostClass::X86_64Avx512),
1032 DispatchCosts::for_class(HostClass::X86_64Avx2),
1033 DispatchCosts::for_class(HostClass::Aarch64Apple),
1034 DispatchCosts::for_class(HostClass::Generic).with(DispatchKind::Team, WidthCosts::flat(us(1))),
1035 ];
1036 for costs in rows {
1037 let s = ParShape::Team(nz(128), costs);
1038 for w in 2..=128 {
1039 assert_eq!(s.dispatch_cost(nz(w)), costs.cost(DispatchKind::Team, nz(w)), "{costs:?} width {w}");
1040 }
1041 }
1042 let flat = ParShape::Team(nz(128), rows[4]);
1045 for (w, ns) in [(8, 1333), (12, 2000), (16, 2666), (32, 5333), (64, 10666), (128, 21333)] {
1046 assert_eq!(flat.dispatch_cost(nz(w)), Duration::from_nanos(ns), "flat width {w}");
1047 }
1048 let avx512 = ParShape::Team(nz(128), rows[1]);
1049 assert_eq!(avx512.dispatch_cost(nz(8)), Duration::from_nanos(980));
1050 assert_eq!(avx512.dispatch_cost(nz(64)), Duration::from_nanos(980 * 8));
1051 let zen2 = ParShape::Team(nz(128), rows[2]);
1052 assert_eq!(zen2.dispatch_cost(nz(64)), Duration::from_nanos(9333));
1053 assert_eq!(zen2.dispatch_cost(nz(128)), Duration::from_nanos(18666));
1054 let at_8 = flat.dispatch_cost(nz(8)) * 3;
1056 assert!(flat.pays(at_8, 1, nz(8)));
1057 assert!(!flat.pays(at_8, 1, nz(64)));
1058 }
1059
1060 #[test]
1061 fn fixed_and_never_keep_their_meaning() {
1062 let r = rate::lu_full_pivot();
1063 assert_eq!(ParThreshold::Fixed(100).region_width::<f64>(&crate::Par::Seq, 100, r, 1), 1);
1065 crate::team::with_team(6, |par| {
1066 assert_eq!(ParThreshold::Never.region_width::<f64>(&par, usize::MAX, r, 1), 1);
1067 assert_eq!(ParThreshold::Fixed(100).region_width::<f64>(&par, 99, r, 1), 1);
1068 assert_eq!(ParThreshold::Fixed(100).region_width::<f64>(&par, 100, r, 1), 6);
1069 });
1070 assert_eq!(ParThreshold::from(0), ParThreshold::Derived);
1072 assert_eq!(ParThreshold::from(usize::MAX), ParThreshold::Never);
1073 assert_eq!(ParThreshold::from(7), ParThreshold::Fixed(7));
1074 assert_eq!(usize::from(ParThreshold::Fixed(0)), 1);
1075 }
1076
1077 #[test]
1078 fn derived_gates_never_pay_on_a_sequential_shape() {
1079 let seq = crate::Par::Seq;
1080 for (_, r, sections) in gates(rates()) {
1081 assert_eq!(ParThreshold::Derived.region_width::<f64>(&seq, usize::MAX / 2, r, sections), 1);
1082 assert_eq!(ParThreshold::Derived.region_width::<c64>(&seq, usize::MAX / 2, r, sections), 1);
1083 assert!(!ParShape::Seq.pays_full(serial_time(usize::MAX / 2, r.of::<f64>()), sections));
1084 }
1085 }
1086
1087 #[test]
1088 fn derived_floors_on_a_team_follow_the_rule() {
1089 use threadteam::DispatchCosts;
1090 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1091 let d = team.dispatch_cost(nz(6)).as_secs_f64() * 1e9;
1092 for (name, r, sections) in gates(M5) {
1093 for (ty, ns) in [("f64", r.real), ("c64", r.cplx)] {
1094 let floor = if ty == "f64" {
1095 floor_units::<f64>(team, r, sections)
1096 } else {
1097 floor_units::<c64>(team, r, sections)
1098 };
1099 let want = PAYS * f64::from(sections) * d / ns;
1101 assert!(
1102 (floor as f64 - want).abs() <= 4.0 + want * 2e-3,
1103 "{name} {ty}: floor {floor}, rule {want}"
1104 );
1105 }
1106 }
1107 let pays = |u: usize, r: Rate, f: u32| {
1109 team.pays_full(serial_time(u, r.of::<f64>()), f)
1110 };
1111 assert!(!pays(100 * 100, M5.lu_full_pivot, 1));
1113 assert!(pays(200 * 200, M5.lu_full_pivot, 1));
1114 assert!(floor_units::<f64>(team, M5.bidiag, 2) >= 2 * floor_units::<f64>(team, M5.bidiag, 1) - 2);
1116 assert!(pays(256 * 256, M5.matvec, 1));
1118 assert!(!pays(64 * 64, M5.matvec, 1));
1119 let lu = floor_units::<f64>(team, M5.lu_full_pivot, 1);
1122 let bidiag = floor_units::<f64>(team, M5.bidiag, 1);
1123 assert!(lu > bidiag);
1124 }
1125
1126 #[test]
1127 fn par_width_reads_the_degree_of_the_token() {
1128 let seq = crate::Par::Seq;
1129 assert_eq!(par_width::<f64>(&seq, usize::MAX / 2, rate::matvec(), 1), 1);
1130 crate::team::with_team(6, |par| {
1131 assert_eq!(par_width::<f64>(&par, 10_000_000, rate::matvec(), 1), 6);
1132 assert_eq!(par_width::<f64>(&par, 1_000, rate::matvec(), 1), 1);
1133 assert_eq!(par_width_upto::<f64>(&par, 10_000_000, rate::matvec(), 1, 3), 3);
1135 });
1136 crate::team::with_team(1, |par| {
1137 assert_eq!(par_width::<f64>(&par, 10_000_000, rate::matvec(), 1), 1);
1138 });
1139 }
1140
1141 #[test]
1142 fn rates_are_per_dtype() {
1143 let r = rate::bidiag();
1144 assert_eq!(r.of::<f64>(), r.real);
1145 assert_eq!(r.of::<c64>(), r.cplx);
1146 assert_eq!(r.of::<f32>(), r.real / 2.0);
1147 assert_eq!(r.of::<crate::c32>(), r.cplx / 2.0);
1148 assert_eq!(rate::gemm_mac().of::<f64>(), ns_per_mac::f64());
1149 assert_eq!(rate::gemm_mac().of::<c64>(), ns_per_mac::c64());
1150 }
1151
1152 #[cfg(feature = "rayon")]
1156 mod forced_parallel {
1157 use super::*;
1158 use crate::dyn_stack::{MemBuffer, MemStack};
1159 use crate::internal_prelude::*;
1160 use crate::linalg::cholesky::lblt::factor as bk;
1161 use crate::linalg::evd::{hessenberg, tridiag};
1162 use crate::linalg::lu::full_pivoting::factor as fplu;
1163 use crate::linalg::qr::col_pivoting::factor as cpqr;
1164 use crate::linalg::svd::bidiag;
1165 use crate::{Auto, Mat, MatRef, Par};
1166
1167 const N: usize = 96;
1168
1169 fn fill<T: crate::ComplexField>(n: usize, seed: usize) -> Mat<T> {
1170 Mat::from_fn(n, n, |i, j| {
1171 let h = (i * 2654435761usize)
1172 .wrapping_add(j * 40503)
1173 .wrapping_add(seed * 977)
1174 % 1009;
1175 from_f64::<T>(h as f64 / 504.0 - 1.0)
1176 })
1177 }
1178 fn close<T: crate::ComplexField<Real = f64>>(
1179 what: &str,
1180 a: MatRef<'_, T>,
1181 b: MatRef<'_, T>,
1182 ) {
1183 let mut err = 0.0f64;
1184 for j in 0..a.ncols() {
1185 for i in 0..a.nrows() {
1186 err = err.max((a[(i, j)].clone() - b[(i, j)].clone()).abs());
1187 }
1188 }
1189 core::assert!(err <= 1e-9, "{what}: error {err}");
1190 }
1191
1192 fn check<T: crate::ComplexField<Real = f64>>(compare_complex_factors: bool) {
1193 let a0 = fill::<T>(N, 1);
1194 let sym = &a0 + a0.adjoint();
1195 let lu = |par: Par| {
1197 let mut a = a0.clone();
1198 let mut p = vec![0usize; 4 * N];
1199 let (r, rest) = p.split_at_mut(N);
1200 let (ri, rest) = rest.split_at_mut(N);
1201 let (c, ci) = rest.split_at_mut(N);
1202 let mut params: fplu::FullPivLuParams = Auto::<T>::auto();
1203 params.par_threshold = ParThreshold::Fixed(1);
1204 let mut mem = MemBuffer::new(fplu::lu_in_place_scratch::<usize, T>(
1205 N,
1206 N,
1207 par.shape(),
1208 params.into(),
1209 ));
1210 fplu::lu_in_place(
1211 a.as_mut(), r, ri, c, ci, par, MemStack::new(&mut mem), params.into(),
1212 );
1213 a
1214 };
1215 crate::team::with_team(4, |par| close("lu", lu(Par::Seq).as_ref(), lu(par).as_ref()));
1216 let bunch = |par: Par| {
1217 let mut a = sym.clone();
1218 let mut perm = vec![0usize; N];
1219 let mut perm_inv = vec![0usize; N];
1220 let mut sub = Mat::<T>::zeros(N, 1);
1221 let mut params: bk::LbltParams = Auto::<T>::auto();
1222 params.pivoting = bk::PivotingStrategy::Full;
1223 params.par_threshold = ParThreshold::Fixed(1);
1224 let mut mem = MemBuffer::new(bk::cholesky_in_place_scratch::<usize, T>(
1225 N,
1226 par.shape(),
1227 params.into(),
1228 ));
1229 bk::cholesky_in_place(
1230 a.as_mut(),
1231 sub.as_mut().col_mut(0).as_diagonal_mut(),
1232 &mut perm,
1233 &mut perm_inv,
1234 par,
1235 MemStack::new(&mut mem),
1236 params.into(),
1237 );
1238 a
1239 };
1240 crate::team::with_team(4, |par| close("bunch", bunch(Par::Seq).as_ref(), bunch(par).as_ref()));
1241 let qr = |par: Par| {
1242 let mut a = a0.clone();
1243 let mut h = Mat::<T>::zeros(16, N);
1244 let mut p = vec![0usize; N];
1245 let mut pi = vec![0usize; N];
1246 let mut params: cpqr::ColPivQrParams = Auto::<T>::auto();
1247 params.par_threshold = ParThreshold::Fixed(1);
1248 let mut mem = MemBuffer::new(cpqr::qr_in_place_scratch::<usize, T>(
1249 N,
1250 N,
1251 16,
1252 par.shape(),
1253 params.into(),
1254 ));
1255 cpqr::qr_in_place(
1256 a.as_mut(), h.as_mut(), &mut p, &mut pi, par, MemStack::new(&mut mem), params.into(),
1257 );
1258 a
1259 };
1260 crate::team::with_team(4, |par| close("qr", qr(Par::Seq).as_ref(), qr(par).as_ref()));
1261 let bid = |par: Par| {
1262 let mut a = a0.clone();
1263 let mut hl = Mat::<T>::zeros(8, N);
1264 let mut hr = Mat::<T>::zeros(8, N - 1);
1265 let mut params: bidiag::BidiagParams = Auto::<T>::auto();
1266 params.par_threshold = ParThreshold::Fixed(1);
1267 let mut mem = MemBuffer::new(bidiag::bidiag_in_place_scratch::<T>(
1268 N,
1269 N,
1270 par.shape(),
1271 params.into(),
1272 ));
1273 bidiag::bidiag_in_place(
1274 a.as_mut(), hl.as_mut(), hr.as_mut(), par, MemStack::new(&mut mem), params.into(),
1275 );
1276 for j in 0..N {
1281 for i in 0..N {
1282 if i > j || j > i + 1 {
1283 a[(i, j)] = T::zero();
1284 }
1285 }
1286 }
1287 a
1288 };
1289 if compare_complex_factors {
1296 crate::team::with_team(4, |par| {
1297 close("bid", bid(Par::Seq).as_ref(), bid(par).as_ref())
1298 });
1299 } else {
1300 let seq = bid(Par::Seq);
1304 let team = crate::team::with_team(4, |par| bid(par));
1305 core::assert!(seq.as_ref().is_all_finite(), "bid sequential");
1306 core::assert!(team.as_ref().is_all_finite(), "bid team");
1307 }
1308 let hess = |par: Par| {
1309 let mut a = a0.clone();
1310 let mut h = Mat::<T>::zeros(8, N - 1);
1311 let mut params: hessenberg::HessenbergParams = Auto::<T>::auto();
1312 params.par_threshold = ParThreshold::Fixed(1);
1313 params.blocking_threshold = usize::MAX;
1314 let mut mem = MemBuffer::new(hessenberg::hessenberg_in_place_scratch::<T>(
1315 N,
1316 8,
1317 par.shape(),
1318 params.into(),
1319 ));
1320 hessenberg::hessenberg_in_place(
1321 a.as_mut(), h.as_mut(), par, MemStack::new(&mut mem), params.into(),
1322 );
1323 a
1324 };
1325 if compare_complex_factors {
1330 crate::team::with_team(4, |par| {
1331 close("hess", hess(Par::Seq).as_ref(), hess(par).as_ref())
1332 });
1333 } else {
1334 let seq = hess(Par::Seq);
1335 let team = crate::team::with_team(4, |par| hess(par));
1336 core::assert!(seq.as_ref().is_all_finite(), "hess sequential");
1337 core::assert!(team.as_ref().is_all_finite(), "hess team");
1338 }
1339 let tri = |par: Par| {
1340 let mut a = sym.clone();
1341 let mut h = Mat::<T>::zeros(8, N - 1);
1342 let mut params: tridiag::TridiagParams = Auto::<T>::auto();
1343 params.par_threshold = ParThreshold::Fixed(1);
1344 let mut mem = MemBuffer::new(tridiag::tridiag_in_place_scratch::<T>(
1345 N,
1346 par.shape(),
1347 params.into(),
1348 ));
1349 tridiag::tridiag_in_place(
1350 a.as_mut(), h.as_mut(), par, MemStack::new(&mut mem), params.into(),
1351 );
1352 a
1353 };
1354 crate::team::with_team(4, |par| close("tri", tri(Par::Seq).as_ref(), tri(par).as_ref()));
1355 }
1356
1357 #[test]
1358 fn forced_parallel_steps_equal_serial_f64() {
1359 check::<f64>(true);
1360 }
1361
1362 #[test]
1363 fn forced_parallel_steps_equal_serial_c64() {
1364 check::<c64>(false);
1365 }
1366 }
1367
1368 #[test]
1370 fn halves_join_gates_follow_the_rule() {
1371 let seq = crate::Par::Seq;
1372 for r in [rate::bidiag_svd_dc(), rate::tridiag_evd_dc(), rate::triangular_inverse()] {
1373 assert!(!halves_pays::<f64>(&seq, 1 << 20, r, 2));
1374 }
1375 crate::team::with_team(6, |par| {
1376 for r in [rate::bidiag_svd_dc(), rate::tridiag_evd_dc(), rate::triangular_inverse()] {
1377 let power = if r == rate::triangular_inverse() { 3 } else { 2 };
1379 let first = (2..4096).find(|&n| halves_pays::<f64>(&par, n, r, power)).expect("never pays");
1380 assert!(first > 1, "{first}");
1383 assert!(!halves_pays::<f64>(&par, first - 1, r, power));
1384 assert!(halves_pays::<f64>(&par, first, r, power));
1385 assert!(halves_pays::<f64>(&par, 2 * first, r, power));
1386 let h = first / 2;
1388 let work_ns = 2.0 * (h as f64).powi(power as i32) * r.real;
1389 let want = PAYS * par.shape().dispatch_cost(nz(2)).as_secs_f64() * 1e9;
1390 assert!(work_ns >= want * 0.999, "{work_ns} {want}");
1391 }
1392 });
1393 }
1394
1395 #[test]
1402 fn block_apply_split_starts_where_the_estimate_pays() {
1403 use threadteam::DispatchCosts;
1404 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1405 let pays = |m: usize| {
1406 let (n, k) = (8, m - 8);
1407 pays_at(block_apply_estimate_with::<f64>(M5, m, n, k), 1, team.dispatch_cost(nz(6)))
1408 };
1409 assert!(!pays(72));
1410 assert!(pays(80));
1411 let gemm_only = |m: usize| {
1413 pays_at(serial_time(2 * m * 8 * (m - 8), M5.gemm_mac.real), 1, team.dispatch_cost(nz(6)))
1414 };
1415 assert!(!gemm_only(88) && gemm_only(96));
1416 let c = block_apply_estimate_with::<c64>(M5, 100, 8, 92).as_secs_f64() * 1e9;
1418 let want = 2.0 * 100.0 * 8.0 * 92.0 * M5.block_apply_mac.cplx + 100.0 * 92.0 * M5.householder_rank_one.cplx;
1419 assert!((c - want).abs() < 2.0, "{c} {want}");
1420 }
1421
1422 #[test]
1423 fn join_gates_on_a_team_follow_the_width_two_cost() {
1424 use threadteam::DispatchCosts;
1425 let team = ParShape::Team(nz(6), DispatchCosts::for_class(threadteam::HostClass::Aarch64Apple));
1426 for (name, r, sections) in gates(M5) {
1427 assert!(floor_units::<f64>(team, r, sections) > 0, "{name}");
1428 }
1429 let d2 = team.dispatch_cost(nz(2));
1431 assert_eq!(d2, Duration::from_micros(3));
1432 let par = |ns: f64| pays_at(Duration::from_secs_f64(ns * 1e-9), 1, d2);
1433 assert!(par(9000.0) && !par(8900.0));
1434 for (r, power, yes, no) in [
1438 (M5.bidiag_svd_dc, 2u32, 128usize, 8usize),
1439 (M5.tridiag_evd_dc, 2, 64, 8),
1440 (M5.triangular_inverse, 3, 256, 64),
1441 ] {
1442 let pays = |n: usize| {
1443 let h = n / 2;
1444 pays_at(
1445 serial_time(2 * h.pow(power), r.real),
1446 1,
1447 d2,
1448 )
1449 };
1450 assert!(pays(yes));
1451 assert!(!pays(no));
1452 }
1453 assert!(team.pays_full(serial_time(1_000_000, M5.sparse_matmul.of::<f64>()), 1));
1455 assert!(!team.pays_full(serial_time(1_000, M5.sparse_matmul.of::<f64>()), 1));
1456 }
1457}
1458
1459#[cfg(feature = "par-trace")]
1460#[doc(hidden)]
1461pub fn trace_pub(
1462 units: usize,
1463 sections: u32,
1464 estimate: Duration,
1465 dispatch: Duration,
1466 width: usize,
1467 r: bool,
1468 loc: &'static core::panic::Location<'static>,
1469) {
1470 trace::record(loc, units, sections, estimate.as_secs_f64() * 1e9, dispatch.as_secs_f64() * 1e9, width, r);
1471}
1472
1473#[cfg(all(test, feature = "sparse-linalg"))]
1474mod join_results {
1475 use crate::dyn_stack::{MemBuffer, MemStack};
1478 use crate::linalg::evd::{self, ComputeEigenvectors};
1479 use crate::linalg::svd;
1480 use crate::linalg::triangular_inverse::{invert_lower_triangular, invert_unit_lower_triangular};
1481 use crate::{Accum, Mat, Par, Spec};
1482
1483 struct Rng(u64);
1484 impl Rng {
1485 fn next(&mut self) -> f64 {
1486 self.0 ^= self.0 << 13;
1487 self.0 ^= self.0 >> 7;
1488 self.0 ^= self.0 << 17;
1489 (self.0 >> 11) as f64 / (1u64 << 53) as f64 - 0.5
1490 }
1491 }
1492
1493 fn close(a: &[f64], b: &[f64], tol: f64) {
1494 let scale = a.iter().fold(1.0f64, |m, x| m.max(x.abs()));
1495 for (x, y) in a.iter().zip(b) {
1496 assert!((x - y).abs() <= tol * scale, "{x} {y}");
1497 }
1498 }
1499
1500 #[test]
1501 fn triangular_inverse_agrees_with_serial() {
1502 let n = 512;
1503 let mut r = Rng(3);
1504 let mut src = Mat::<f64>::from_fn(n, n, |_, _| r.next() * 0.1);
1505 for i in 0..n {
1506 src[(i, i)] = 2.0 + src[(i, i)].abs();
1507 }
1508 for unit in [false, true] {
1509 let mut a = Mat::<f64>::zeros(n, n);
1510 let mut b = Mat::<f64>::zeros(n, n);
1511 if unit {
1512 invert_unit_lower_triangular(a.as_mut(), src.as_ref(), Par::Seq);
1513 } else {
1514 invert_lower_triangular(a.as_mut(), src.as_ref(), Par::Seq);
1515 }
1516 crate::team::with_team(4, |par| {
1517 if unit {
1518 invert_unit_lower_triangular(b.as_mut(), src.as_ref(), par);
1519 } else {
1520 invert_lower_triangular(b.as_mut(), src.as_ref(), par);
1521 }
1522 });
1523 for j in 0..n {
1524 close(
1525 &(0..n).map(|i| a[(i, j)]).collect::<Vec<_>>(),
1526 &(0..n).map(|i| b[(i, j)]).collect::<Vec<_>>(),
1527 1e-9,
1528 );
1529 }
1530 }
1531 }
1532
1533 #[test]
1534 fn bidiagonal_svd_agrees_with_serial() {
1535 let n = 512;
1536 let mut r = Rng(5);
1537 let d0: Vec<f64> = (0..n).map(|_| r.next() + 1.0).collect();
1538 let e: Vec<f64> = (0..n - 1).map(|_| r.next()).collect();
1539 let mut out = vec![];
1540 let run = |par: Par, out: &mut Vec<Vec<f64>>| {
1541 let mut d = d0.clone();
1542 let mut u = Mat::<f64>::zeros(n, n);
1543 let mut v = Mat::<f64>::zeros(n, n);
1544 let mut mem = MemBuffer::new(svd::bidiagonal_svd_scratch::<f64>(
1545 n,
1546 par.shape(),
1547 Spec::default(),
1548 ));
1549 svd::bidiagonal_svd(
1550 &mut d,
1551 &e,
1552 true,
1553 u.as_mut(),
1554 v.as_mut(),
1555 par,
1556 MemStack::new(&mut mem),
1557 Spec::default(),
1558 )
1559 .unwrap();
1560 out.push(d);
1561 };
1562 run(Par::Seq, &mut out);
1563 crate::team::with_team(4, |par| run(par, &mut out));
1564 close(&out[0], &out[1], 1e-10);
1565 }
1566
1567 #[test]
1568 fn tridiagonal_evd_agrees_with_serial() {
1569 let n = 512;
1570 let mut r = Rng(6);
1571 let d0: Vec<f64> = (0..n).map(|_| r.next()).collect();
1572 let mut e0: Vec<f64> = (0..n - 1).map(|_| r.next()).collect();
1573 e0.push(0.0);
1574 let mut out = vec![];
1575 let run = |par: Par, out: &mut Vec<Vec<f64>>| {
1576 let mut u = Mat::<f64>::zeros(n, n);
1577 let mut s = Mat::<f64>::zeros(n, 1);
1578 let mut mem = MemBuffer::new(evd::tridiagonal_self_adjoint_evd_scratch::<f64>(
1579 n,
1580 ComputeEigenvectors::Yes,
1581 par.shape(),
1582 Spec::default(),
1583 ));
1584 let d = crate::col::ColRef::from_slice(&d0);
1585 let e = crate::col::ColRef::from_slice(&e0);
1586 evd::tridiagonal_self_adjoint_evd(
1587 d.as_diagonal(),
1588 e.as_diagonal(),
1589 s.as_mut().col_mut(0).as_diagonal_mut(),
1590 Some(u.as_mut()),
1591 par,
1592 MemStack::new(&mut mem),
1593 Spec::default(),
1594 )
1595 .unwrap();
1596 out.push((0..n).map(|i| s[(i, 0)]).collect::<Vec<_>>());
1597 };
1598 run(Par::Seq, &mut out);
1599 crate::team::with_team(4, |par| run(par, &mut out));
1600 close(&out[0], &out[1], 1e-10);
1601 }
1602
1603 #[test]
1604 fn sparse_matmul_agrees_with_serial() {
1605 use crate::sparse::SparseColMat;
1606 use crate::sparse::Triplet;
1607 use crate::sparse::linalg::matmul::sparse_sparse_matmul;
1608 let n = 6000;
1609 let mk = |seed: u64| {
1610 let mut r = Rng(seed);
1611 let mut t = vec![];
1612 for j in 0..n {
1613 for _ in 0..24 {
1614 let i = ((r.next() + 0.5) * n as f64) as usize % n;
1615 t.push(Triplet::new(i, j, r.next()));
1616 }
1617 }
1618 SparseColMat::<usize, f64>::try_new_from_triplets(n, n, &t).unwrap()
1619 };
1620 let (a, b) = (mk(1), mk(2));
1621 let s = sparse_sparse_matmul(a.as_ref(), b.as_ref(), 1.0, Par::Seq).unwrap();
1622 let p = crate::team::with_team(4, |par| {
1623 sparse_sparse_matmul(a.as_ref(), b.as_ref(), 1.0, par).unwrap()
1624 });
1625 close(s.val(), p.val(), 1e-12);
1626 let _ = Accum::Add;
1627 }
1628}