1use core::cell::UnsafeCell;
16use rayon::iter::plumbing::{Producer, ProducerCallback};
17use rayon::iter::{IndexedParallelIterator, IntoParallelIterator};
18use threadteam::TeamExec;
19
20pub fn with_team<R>(width: usize, f: impl FnOnce(crate::Par<'_>) -> R) -> R {
80 #[cfg(test)]
83 let _gate = team_turn();
84 let width = width.max(1);
85 if width == 1 {
86 return f(crate::Par::Seq);
87 }
88 let pool = threadteam::Pool::builder()
89 .threads(width)
90 .acquire_timeout(core::time::Duration::from_secs(30))
91 .build()
92 .expect("a thread pool for the team");
93 let team = pool
94 .into_team(width)
95 .expect("an idle pool: it was just created");
96 let mut exec = team.exec(width);
97 exec.team(|team| f(crate::Par::Team(team)))
98 .ok()
99 .expect("a team token for an idle team")
100}
101
102
103#[cfg(test)]
110pub(crate) static TEAM_GATE: std::sync::Mutex<()> = std::sync::Mutex::new(());
111
112#[cfg(test)]
114pub(crate) struct TeamTurn(Option<std::sync::MutexGuard<'static, ()>>);
115#[cfg(test)]
116thread_local! {
117 static TEAM_DEPTH: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
118}
119#[cfg(test)]
120impl Drop for TeamTurn {
121 fn drop(&mut self) {
122 TEAM_DEPTH.with(|d| d.set(d.get() - 1));
123 drop(self.0.take());
124 }
125}
126#[cfg(test)]
128pub(crate) fn team_turn() -> TeamTurn {
129 let nested = TEAM_DEPTH.with(|d| {
130 d.set(d.get() + 1);
131 d.get() > 1
132 });
133 if nested {
134 TeamTurn(None)
135 } else {
136 TeamTurn(Some(TEAM_GATE.lock().unwrap_or_else(|e| e.into_inner())))
137 }
138}
139
140pub mod stats {
146 use alloc::vec::Vec;
147 use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
148 pub(super) static ON: AtomicBool = AtomicBool::new(false);
149 const W: usize = 17;
150 #[allow(clippy::declare_interior_mutable_const)]
151 const Z: AtomicUsize = AtomicUsize::new(0);
152 pub(super) static RUN: [AtomicUsize; W] = [Z; W];
154 pub(super) static SPLIT: [AtomicUsize; W] = [Z; W];
156 const WW: usize = 257;
158 pub(super) static WIDTH: [AtomicUsize; WW] = [Z; WW];
160 pub(super) static GEMM_BELOW_FLOOR: AtomicUsize = AtomicUsize::new(0);
161 pub(super) static GEMM_ONE_TILE: AtomicUsize = AtomicUsize::new(0);
162 pub(super) static GEMM_TILED: AtomicUsize = AtomicUsize::new(0);
163 pub(super) static GEMM_TEAM_KERNEL: AtomicUsize = AtomicUsize::new(0);
164 pub(super) fn hist<const N: usize>(h: &[AtomicUsize; N], n: usize) {
165 if ON.load(Relaxed) {
166 h[n.min(N - 1)].fetch_add(1, Relaxed);
167 }
168 }
169 pub(super) fn bump(c: &AtomicUsize) {
170 if ON.load(Relaxed) {
171 c.fetch_add(1, Relaxed);
172 }
173 }
174 #[derive(Debug, Clone, Default)]
176 pub struct Stats {
177 pub run: Vec<usize>,
179 pub split: Vec<usize>,
181 pub width: Vec<usize>,
183 pub gemm_below_floor: usize,
185 pub gemm_one_tile: usize,
187 pub gemm_tiled: usize,
189 pub gemm_team_kernel: usize,
192 }
193 pub fn enable(on: bool) {
195 ON.store(on, Relaxed);
196 }
197 pub fn take() -> Stats {
199 fn t<const N: usize>(h: &[AtomicUsize; N]) -> Vec<usize> {
200 h.iter().map(|c| c.swap(0, Relaxed)).collect()
201 }
202 Stats {
203 run: t(&RUN),
204 split: t(&SPLIT),
205 width: t(&WIDTH),
206 gemm_below_floor: GEMM_BELOW_FLOOR.swap(0, Relaxed),
207 gemm_one_tile: GEMM_ONE_TILE.swap(0, Relaxed),
208 gemm_tiled: GEMM_TILED.swap(0, Relaxed),
209 gemm_team_kernel: GEMM_TEAM_KERNEL.swap(0, Relaxed),
210 }
211 }
212}
213
214pub(crate) fn note_width(width: usize) {
216 stats::hist(&stats::WIDTH, width);
217}
218
219pub(crate) fn note_split(parts: usize) {
221 stats::hist(&stats::SPLIT, parts);
222}
223
224pub(crate) fn run(team: TeamExec<'_>, n: usize, f: &(dyn Fn(usize, usize) + Sync)) {
227 let size = team.size().get();
228 let m = n.min(size);
229 if m <= 1 {
230 stats::hist(&stats::RUN, 1);
231 return f(0, 1);
232 }
233 stats::hist(&stats::RUN, m);
234 if m == size {
235 team.run(|i, k| f(i, k));
236 } else {
237 let _ = team.split(&[m], |_c, child| {
238 child.run(|i, k| f(i, k));
239 });
240 }
241}
242
243pub(crate) fn for_each<T: Send, I>(
246 team: TeamExec<'_>,
247 n_jobs: usize,
248 iter: I,
249 f: impl Sync + Fn(T),
250) where
251 I: IntoParallelIterator<Item = T>,
252 I::Iter: IndexedParallelIterator,
253{
254 for_each_child(team, n_jobs, iter, |item, _child| f(item))
255}
256
257pub(crate) fn for_each_child<T: Send, I>(
264 team: TeamExec<'_>,
265 n_jobs: usize,
266 iter: I,
267 f: impl Sync + for<'c> Fn(T, TeamExec<'c>),
268) where
269 I: IntoParallelIterator<Item = T>,
270 I::Iter: IndexedParallelIterator,
271{
272 let iter = iter.into_par_iter();
273 let len = iter.len();
274 if len == 0 {
275 return;
276 }
277 let jobs = n_jobs.max(1).min(len).min(team.size().get());
278 stats::hist(&stats::SPLIT, jobs);
279 struct Deal<'t, 'f, F> {
280 team: TeamExec<'t>,
281 len: usize,
282 jobs: usize,
283 f: &'f F,
284 }
285 impl<T, F: Sync + for<'c> Fn(T, TeamExec<'c>)> ProducerCallback<T> for Deal<'_, '_, F> {
286 type Output = ();
287 fn callback<P: Producer<Item = T>>(self, producer: P) {
288 deal(self.team, producer, self.len, self.jobs, self.f)
291 }
292 }
293 iter.with_producer(Deal { team, len, jobs, f: &f });
294}
295
296const FLAT_JOBS: usize = 64;
301const FLAT_BYTES: usize = 256;
302
303struct Piece<P>(UnsafeCell<Option<P>>);
305unsafe impl<P: Send> Sync for Piece<P> {}
307impl<P> Piece<P> {
308 unsafe fn take(&self) -> P {
311 unsafe { (*self.0.get()).take() }.expect("each piece is taken once")
312 }
313}
314
315fn deal<T, P: Producer<Item = T>, F: Sync + for<'c> Fn(T, TeamExec<'c>)>(
318 mut team: TeamExec<'_>,
319 producer: P,
320 len: usize,
321 jobs: usize,
322 f: &F,
323) {
324 if jobs <= 1 {
325 for item in producer.into_iter() {
326 f(item, team.reborrow());
327 }
328 return;
329 }
330 if jobs <= FLAT_JOBS && core::mem::size_of::<Option<P>>() <= FLAT_BYTES {
331 let mut pieces: [Piece<P>; FLAT_JOBS] = core::array::from_fn(|_| Piece(UnsafeCell::new(None)));
332 let mut rest = producer;
333 for (c, piece) in pieces.iter_mut().enumerate().take(jobs - 1) {
334 let (_, n) = super::thread::par_split_indices(len, c, jobs);
336 let (head, tail) = rest.split_at(n);
337 *piece.0.get_mut() = Some(head);
338 rest = tail;
339 }
340 *pieces[jobs - 1].0.get_mut() = Some(rest);
341 team.fork(jobs, |c, mut child| {
342 let piece = unsafe { pieces[c].take() };
344 for item in piece.into_iter() {
345 f(item, child.reborrow());
346 }
347 });
348 return;
349 }
350 let left = jobs / 2;
352 let (left_len, _) = super::thread::par_split_indices(len, left, jobs);
353 let size = team.size().get();
354 let (left_size, _) = super::thread::par_split_indices(size, left, jobs);
355 let (a, b) = producer.split_at(left_len);
356 let halves = [Piece(UnsafeCell::new(Some(a))), Piece(UnsafeCell::new(Some(b)))];
357 note_split(2);
358 team.split(&[left_size, size - left_size], |c, child| {
359 let half = unsafe { halves[c].take() };
361 if c == 0 {
362 deal(child, half, left_len, left, f)
363 } else {
364 deal(child, half, len - left_len, jobs - left, f)
365 }
366 })
367 .expect("two non-empty parts within the team");
368}
369
370struct SendPtr<T>(*mut T);
371unsafe impl<T> Send for SendPtr<T> {}
372unsafe impl<T> Sync for SendPtr<T> {}
373impl<T> Clone for SendPtr<T> {
374 fn clone(&self) -> Self {
375 *self
376 }
377}
378impl<T> Copy for SendPtr<T> {}
379impl<T> SendPtr<T> {
380 fn get(self) -> *mut T {
381 self.0
382 }
383}
384
385const ROW_GRAIN: usize = 8;
389const COL_GRAIN: usize = 4;
390
391pub(crate) fn tile_grid(m: usize, n: usize, nt: usize) -> (usize, usize) {
395 let max_pm = (m / ROW_GRAIN).max(1);
396 let max_pn = (n / COL_GRAIN).max(1);
397 let mut best = (1, 1);
398 let mut best_used = 1;
399 let mut best_cost = f64::INFINITY;
400 for pm in 1..=nt.min(max_pm) {
401 for pn in 1..=(nt / pm).min(max_pn) {
402 let used = pm * pn;
403 let cost = m as f64 / pm as f64 + n as f64 / pn as f64;
404 if used > best_used || (used == best_used && cost < best_cost) {
406 best = (pm, pn);
407 best_used = used;
408 best_cost = cost;
409 }
410 }
411 }
412 best
413}
414
415fn grain_range(len: usize, parts: usize, idx: usize, grain: usize) -> (usize, usize) {
418 let units = len.div_ceil(grain);
419 let edge = |i: usize| -> usize {
420 let base = units / parts;
421 let rem = units % parts;
422 let u = if i < rem { i * (base + 1) } else { rem + i * base };
423 (u * grain).min(len)
424 };
425 (edge(idx), edge(idx + 1))
426}
427
428#[cfg(all(target_arch = "x86_64", feature = "std"))]
430fn x86_kernel<E: 'static>() -> Option<(private_gemm_x86::InstrSet, private_gemm_x86::DType)> {
431 use core::any::TypeId;
432 use private_gemm_x86::*;
433 let id = TypeId::of::<E>();
434 let dtype = if id == TypeId::of::<f64>() {
435 DType::F64
436 } else if id == TypeId::of::<f32>() {
437 DType::F32
438 } else if id == TypeId::of::<num_complex::Complex<f64>>() {
439 DType::C64
440 } else if id == TypeId::of::<num_complex::Complex<f32>>() {
441 DType::C32
442 } else {
443 return None;
444 };
445 if std::arch::is_x86_feature_detected!("avx512f") {
446 Some((InstrSet::Avx512, dtype))
447 } else if std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma") {
448 Some((InstrSet::Avx256, dtype))
449 } else {
450 None
451 }
452}
453
454#[cfg(all(target_arch = "x86_64", feature = "std"))]
459pub(crate) mod kernel {
460 use super::{stats, x86_kernel};
461 use core::cell::{RefCell, UnsafeCell};
462 use core::time::Duration;
463 use private_gemm_x86::Fork;
464 use threadteam::TeamExec;
465
466 struct TeamFork<'t> {
471 team: RefCell<TeamExec<'t>>,
472 width: usize,
473 }
474
475 impl Fork for TeamFork<'_> {
476 fn width(&self) -> usize {
477 self.width
478 }
479
480 fn run(&self, f: &(dyn Fn(usize) + Sync)) {
481 stats::hist(&stats::RUN, self.width);
482 self.team.borrow_mut().reborrow().run(|j, _| f(j));
484 }
485 }
486
487 pub(crate) fn kernel_width(team: &TeamExec<'_>, estimate: Duration) -> Option<usize> {
491 match crate::utils::cost::par_width_team(team, estimate, 2, usize::MAX) {
493 1 => {
494 stats::bump(&stats::GEMM_BELOW_FLOOR);
495 None
496 },
497 width => Some(width),
498 }
499 }
500
501 pub(crate) fn with_fork<R>(team: TeamExec<'_>, width: usize, f: impl FnOnce(&dyn Fork) -> R) -> R {
504 stats::bump(&stats::GEMM_TEAM_KERNEL);
505 let size = team.size().get();
506 let width = width.clamp(1, size);
507 if width < size {
508 super::note_width(width);
509 }
510 if width == size {
511 return f(&TeamFork { team: RefCell::new(team), width });
512 }
513 struct Slot<F, R>(UnsafeCell<(Option<F>, Option<R>)>);
514 unsafe impl<F, R> Sync for Slot<F, R> {}
517 impl<F, R> Slot<F, R> {
518 fn get(&self) -> *mut (Option<F>, Option<R>) {
519 self.0.get()
520 }
521 }
522 let slot = Slot(UnsafeCell::new((Some(f), None)));
523 team.split(&[width], |_, child| {
524 let (f, out) = unsafe { &mut *slot.get() };
526 let f = f.take().expect("one child");
527 *out = Some(f(&TeamFork { team: RefCell::new(child), width }));
528 })
529 .expect("1 <= width <= size is a valid split");
530 slot.0.into_inner().1.expect("the child ran")
531 }
532
533 pub(crate) fn available<E: 'static>() -> Option<(private_gemm_x86::InstrSet, private_gemm_x86::DType)> {
535 x86_kernel::<E>()
536 }
537}
538
539#[allow(clippy::too_many_arguments)]
549pub(crate) unsafe fn gemm_team<E: 'static + Copy + Send + Sync>(
550 team: TeamExec<'_>,
551 m: usize,
552 n: usize,
553 k: usize,
554 ns_per_mac: f64,
555 dst: *mut E,
556 dst_cs: isize,
557 dst_rs: isize,
558 read_dst: bool,
559 lhs: *const E,
560 lhs_cs: isize,
561 lhs_rs: isize,
562 rhs: *const E,
563 rhs_cs: isize,
564 rhs_rs: isize,
565 alpha: E,
566 beta: E,
567 conj_lhs: bool,
568 conj_rhs: bool,
569) {
570 #[cfg(all(target_arch = "x86_64", feature = "std"))]
571 if let Some((feat, dtype)) = kernel::available::<E>() {
572 use private_gemm_x86::*;
573 macro_rules! go {
577 ($call:path, $last:expr) => {
578 $call(
579 dtype,
580 IType::U64,
581 feat,
582 m,
583 n,
584 k,
585 dst as *mut (),
586 dst_rs,
587 dst_cs,
588 core::ptr::null(),
589 core::ptr::null(),
590 DstKind::Full,
591 if read_dst { Accum::Add } else { Accum::Replace },
592 lhs as *const (),
593 lhs_rs,
594 lhs_cs,
595 conj_lhs,
596 core::ptr::null(),
597 0,
598 rhs as *const (),
599 rhs_rs,
600 rhs_cs,
601 conj_rhs,
602 &raw const beta as *const (),
603 $last,
604 )
605 };
606 }
607 let call = |fork: Option<&dyn Fork>| unsafe {
608 match fork {
609 Some(fork) => go!(gemm_with, fork),
610 None => go!(gemm, 1),
611 }
612 };
613 let estimate = crate::utils::cost::serial_time(m.saturating_mul(n).saturating_mul(k), ns_per_mac);
614 match kernel::kernel_width(&team, estimate) {
615 Some(width) => kernel::with_fork(team, width, |fork| call(Some(fork))),
616 None => call(None),
617 }
618 return;
619 }
620 gemm_tiled::<E>(team, m, n, k, ns_per_mac, dst, dst_cs, dst_rs, read_dst, lhs, lhs_cs, lhs_rs, rhs, rhs_cs, rhs_rs, alpha, beta, conj_lhs, conj_rhs)
621}
622
623#[allow(clippy::too_many_arguments)]
637pub(crate) unsafe fn gemm_tiled<E: 'static + Copy + Send + Sync>(
638 team: TeamExec<'_>,
639 m: usize,
640 n: usize,
641 k: usize,
642 ns_per_mac: f64,
643 dst: *mut E,
644 dst_cs: isize,
645 dst_rs: isize,
646 read_dst: bool,
647 lhs: *const E,
648 lhs_cs: isize,
649 lhs_rs: isize,
650 rhs: *const E,
651 rhs_cs: isize,
652 rhs_rs: isize,
653 alpha: E,
654 beta: E,
655 conj_lhs: bool,
656 conj_rhs: bool,
657) {
658 let seq = |dst: *mut E, lhs: *const E, rhs: *const E, m: usize, n: usize| unsafe {
662 gemm::gemm(
663 m,
664 n,
665 k,
666 dst,
667 dst_cs,
668 dst_rs,
669 read_dst,
670 lhs,
671 lhs_cs,
672 lhs_rs,
673 rhs,
674 rhs_cs,
675 rhs_rs,
676 alpha,
677 beta,
678 false,
679 conj_lhs,
680 conj_rhs,
681 gemm::Parallelism::None,
682 )
683 };
684 let estimate = crate::utils::cost::serial_time(m.saturating_mul(n).saturating_mul(k), ns_per_mac);
688 let nt = crate::utils::cost::par_width_team(&team, estimate, 1, usize::MAX);
689 if nt <= 1 {
690 stats::bump(&stats::GEMM_BELOW_FLOOR);
691 return seq(dst, lhs, rhs, m, n);
692 }
693 let (pm, pn) = tile_grid(m, n, nt);
694 if pm * pn <= 1 {
695 stats::bump(&stats::GEMM_ONE_TILE);
696 return seq(dst, lhs, rhs, m, n);
697 }
698 if pm * pn < nt
701 && !crate::utils::cost::pays_at(estimate, 1, team.dispatch_cost(core::num::NonZeroUsize::new(pm * pn).unwrap()))
702 {
703 stats::bump(&stats::GEMM_BELOW_FLOOR);
704 return seq(dst, lhs, rhs, m, n);
705 }
706 if pm * pn < team.size().get() {
707 note_width(pm * pn);
708 }
709 stats::bump(&stats::GEMM_TILED);
710 let (dst, lhs, rhs) = (
711 SendPtr(dst),
712 SendPtr(lhs as *mut E),
713 SendPtr(rhs as *mut E),
714 );
715 run(team, pm * pn, &|i, parts| {
716 let mut t = i;
718 while t < pm * pn {
719 let (ti, tj) = (t % pm, t / pm);
720 let (i0, i1) = grain_range(m, pm, ti, ROW_GRAIN);
721 let (j0, j1) = grain_range(n, pn, tj, COL_GRAIN);
722 if i1 > i0 && j1 > j0 {
723 let d = dst.get().wrapping_offset(i0 as isize * dst_rs + j0 as isize * dst_cs);
724 let l = lhs.get().wrapping_offset(i0 as isize * lhs_rs) as *const E;
725 let r = rhs.get().wrapping_offset(j0 as isize * rhs_cs) as *const E;
726 seq(d, l, r, i1 - i0, j1 - j0);
727 }
728 t += parts;
729 }
730 });
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use crate::linalg::matmul::matmul;
737 use crate::{c64, Accum, Mat, MatRef, Par};
738 use core::sync::atomic::{AtomicUsize, Ordering};
739 use threadteam::Pool;
740 use core::time::Duration as StdDuration;
741
742 fn team_of(width: usize) -> threadteam::Team {
743 Pool::builder()
744 .threads(width)
745 .acquire_timeout(std::time::Duration::from_secs(30))
746 .build()
747 .unwrap()
748 .into_team(width)
749 .expect("an idle pool")
750 }
751
752 use super::team_turn;
754
755 fn with_par<R>(width: usize, f: impl FnOnce(Par<'_>) -> R) -> R {
757 let _turn = team_turn();
758 let team = team_of(width);
759 let mut exec = team.exec(width);
760 exec.team(|team| f(Par::Team(team))).ok().expect("a team token")
761 }
762
763 #[test]
767 fn with_team_has_width_participants() {
768 for width in [2usize, 3, 4] {
769 let size = with_team(width, |par| match par {
770 Par::Team(t) => t.size().get(),
771 _ => 1,
772 });
773 assert!(size == width);
774 }
775 let pool = Pool::builder()
776 .threads(3)
777 .acquire_timeout(std::time::Duration::from_secs(30))
778 .build()
779 .unwrap();
780 let team = pool.into_team(4).ok().expect("an idle pool");
781 assert!(team.size().get() == 3);
782 }
783
784 fn rand_mat(m: usize, n: usize, seed: u64) -> Mat<f64> {
785 let mut x = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
786 Mat::from_fn(m, n, |_, _| {
787 x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
788 ((x >> 11) as f64 / (1u64 << 53) as f64) - 0.5
789 })
790 }
791
792 fn max_diff(a: MatRef<'_, f64>, b: MatRef<'_, f64>) -> f64 {
793 let mut d: f64 = 0.0;
794 for j in 0..a.ncols() {
795 for i in 0..a.nrows() {
796 d = d.max((a[(i, j)] - b[(i, j)]).abs());
797 }
798 }
799 d
800 }
801
802 #[test]
807 fn small_qr_on_a_team_is_the_sequential_one() {
808 use crate::dyn_stack::{MemBuffer, MemStack};
809 use crate::linalg::qr::no_pivoting::factor as qrf;
810 let n = 32;
811 let a0 = rand_mat(n, n, 11);
812 let run = |mut par: Par<'_>| {
813 let bs = qrf::recommended_block_size::<f64>(n, n);
814 let mut a = a0.clone();
815 let mut q = Mat::<f64>::zeros(bs, n);
816 let mut mem = MemBuffer::new(qrf::qr_in_place_scratch::<f64>(n, n, bs, par.shape(), Default::default()));
817 qrf::qr_in_place(a.as_mut(), q.as_mut(), par.reborrow(), MemStack::new(&mut mem), Default::default());
818 (a, q)
819 };
820 let (a_seq, q_seq) = run(Par::Seq);
821 let (a_team, q_team) = with_par(4, |par| {
822 stats::take();
823 stats::enable(true);
824 let r = run(par);
825 stats::enable(false);
826 let s = stats::take();
827 let sections: usize = s.run.iter().skip(2).sum::<usize>() + s.split.iter().skip(2).sum::<usize>();
828 assert_eq!(sections, 0, "a QR {n} opened a section: {s:?}");
829 assert_eq!(s.gemm_tiled + s.gemm_team_kernel, 0, "{s:?}");
830 r
831 });
832 assert_eq!(max_diff(a_seq.as_ref(), a_team.as_ref()), 0.0);
833 assert_eq!(max_diff(q_seq.as_ref(), q_team.as_ref()), 0.0);
834 }
835
836 #[test]
840 #[cfg(all(target_arch = "x86_64", feature = "std"))]
841 fn kernel_width_rejects_at_the_cheapest_width() {
842 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
843 use core::time::Duration;
844 let _turn = team_turn();
845 let us = Duration::from_micros;
846 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic).with(DispatchKind::Team, WidthCosts::new([us(2), us(7), us(12)]));
847 let team = Pool::builder()
848 .threads(6)
849 .acquire_timeout(std::time::Duration::from_secs(30))
850 .dispatch_costs(costs)
851 .build()
852 .unwrap()
853 .into_team(6)
854 .expect("an idle pool");
855 let mut exec = team.exec(6);
856 exec.team(|team| {
857 assert_eq!(kernel::kernel_width(&team, us(13)), Some(2));
858 assert_eq!(kernel::kernel_width(&team, us(11)), None);
859 let par = Par::Team(team);
860 assert!(crate::utils::cost::par_can_pay_time(&par, us(6), 1));
861 assert!(!crate::utils::cost::par_can_pay_time(&par, us(5), 1));
862 })
863 .ok()
864 .expect("a team token");
865 }
866
867 #[test]
871 #[cfg(all(target_arch = "x86_64", feature = "std"))]
872 fn kernel_width_finds_the_widest_paying_width_on_a_falling_row() {
873 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
874 use core::time::Duration;
875 let _turn = team_turn();
876 let us = Duration::from_micros;
877 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic).with(DispatchKind::Team, WidthCosts::new([us(3), us(2), us(1)]));
878 let team = Pool::builder()
879 .threads(6)
880 .acquire_timeout(std::time::Duration::from_secs(30))
881 .dispatch_costs(costs)
882 .build()
883 .unwrap()
884 .into_team(6)
885 .expect("an idle pool");
886 let mut exec = team.exec(6);
887 exec.team(|team| {
888 assert_eq!(kernel::kernel_width(&team, us(7)), Some(6));
889 assert_eq!(kernel::kernel_width(&team, us(5)), None);
890 })
891 .ok()
892 .expect("a team token");
893 }
894
895 fn with_row<R>(width: usize, anchors: &[(usize, StdDuration)], f: impl FnOnce(Par<'_>) -> R) -> R {
897 use threadteam::{DispatchCosts, DispatchKind, WidthCosts};
898 let _turn = team_turn();
899 let costs = DispatchCosts::for_class(threadteam::HostClass::Generic)
900 .with(DispatchKind::Team, WidthCosts::from_anchors(anchors).expect("a valid row"));
901 let team = Pool::builder()
902 .threads(width)
903 .acquire_timeout(std::time::Duration::from_secs(30))
904 .dispatch_costs(costs)
905 .build()
906 .unwrap()
907 .into_team(width)
908 .expect("an idle pool");
909 let mut exec = team.exec(width);
910 exec.team(|team| f(Par::Team(team))).ok().expect("a team token")
911 }
912
913 #[test]
917 fn par_width_takes_the_widest_width_that_pays() {
918 use crate::utils::cost::{par_width_time, par_width_time_upto};
919 let us = StdDuration::from_micros;
920 with_row(8, &[(2, us(1)), (4, us(2)), (8, us(100))], |par| {
923 assert_eq!(par_width_time(&par, us(6), 1), 4);
924 assert_eq!(par_width_time(&par, us(5), 1), 3);
926 assert_eq!(par_width_time(&par, us(2), 1), 1);
927 assert_eq!(par_width_time(&par, us(2000), 1), 8);
928 });
929 with_row(8, &[(2, us(3)), (4, us(9)), (6, us(1)), (8, us(1))], |par| {
931 assert_eq!(par_width_time(&par, us(3), 1), 8);
932 assert_eq!(par_width_time_upto(&par, us(3), 1, 5), 1);
933 assert_eq!(par_width_time_upto(&par, us(9), 1, 5), 2);
934 });
935 assert_eq!(par_width_time(&Par::Seq, us(1_000_000), 1), 1);
936 }
937
938 #[test]
941 fn with_width_runs_on_a_sub_team_of_that_width() {
942 use crate::utils::thread::with_width;
943 with_par(8, |mut par| {
944 stats::enable(true);
945 let _ = stats::take();
946 for (w, want) in [(3, 3), (8, 8), (9, 8), (1, 1), (0, 1)] {
947 let seen = AtomicUsize::new(0);
948 let degree = with_width(par.reborrow(), w, |par| {
949 let degree = par.degree();
950 if let Par::Team(team) = par {
951 team.run(|_, _| {
952 seen.fetch_add(1, Ordering::Relaxed);
953 });
954 }
955 degree
956 });
957 assert_eq!(degree, want, "width {w}");
958 if want > 1 {
959 assert_eq!(seen.load(Ordering::Relaxed), want, "width {w}");
960 }
961 }
962 assert_eq!(par.degree(), 8);
963 stats::enable(false);
964 let s = stats::take();
965 assert_eq!(s.width[3], 1, "{s:?}");
967 assert_eq!(s.width.iter().sum::<usize>(), 1, "{s:?}");
968 });
969 }
970
971 #[test]
974 fn the_families_narrow_their_regions_and_match_sequential() {
975 use crate::dyn_stack::{MemBuffer, MemStack};
976 use crate::linalg;
977 let ns = StdDuration::from_nanos;
978 let row = [(2, ns(10)), (4, ns(10)), (5, StdDuration::from_secs(1))];
980 let tol = |a: MatRef<'_, f64>, b: MatRef<'_, f64>, what: &str| {
981 let scale = (0..a.ncols()).flat_map(|j| (0..a.nrows()).map(move |i| (i, j))).fold(1.0f64, |m, (i, j)| m.max(a[(i, j)].abs()));
982 let d = max_diff(a, b);
983 assert!(d <= 1e-10 * scale, "{what}: {d} against {scale}");
984 };
985 type Run = fn(Par<'_>) -> Vec<Mat<f64>>;
986 let families: [(&str, Run); 14] = [
987 ("full pivoting lu", |par| {
988 let n = 200;
989 let mut a = rand_mat(n, n, 11);
990 let (mut p, mut pi, mut q, mut qi) = (vec![0usize; n], vec![0usize; n], vec![0usize; n], vec![0usize; n]);
991 let req = linalg::lu::full_pivoting::factor::lu_in_place_scratch::<usize, f64>(n, n, par.shape(), Default::default());
992 linalg::lu::full_pivoting::factor::lu_in_place(a.as_mut(), &mut p, &mut pi, &mut q, &mut qi, par, MemStack::new(&mut MemBuffer::new(req)), Default::default());
993 vec![a]
994 }),
995 ("partial pivoting lu", |par| {
996 let n = 400;
997 let mut a = rand_mat(n, n, 12);
998 let (mut p, mut pi) = (vec![0usize; n], vec![0usize; n]);
999 let req = linalg::lu::partial_pivoting::factor::lu_in_place_scratch::<usize, f64>(n, n, par.shape(), Default::default());
1000 linalg::lu::partial_pivoting::factor::lu_in_place(a.as_mut(), &mut p, &mut pi, par, MemStack::new(&mut MemBuffer::new(req)), Default::default());
1001 vec![a]
1002 }),
1003 ("column pivoting qr", |par| {
1004 let n = 200;
1005 let mut a = rand_mat(n, n, 13);
1006 let bs = linalg::qr::no_pivoting::factor::recommended_block_size::<f64>(n, n);
1007 let mut h = Mat::<f64>::zeros(bs, n);
1008 let (mut q, mut qi) = (vec![0usize; n], vec![0usize; n]);
1009 let req = linalg::qr::col_pivoting::factor::qr_in_place_scratch::<usize, f64>(n, n, bs, par.shape(), Default::default());
1010 linalg::qr::col_pivoting::factor::qr_in_place(a.as_mut(), h.as_mut(), &mut q, &mut qi, par, MemStack::new(&mut MemBuffer::new(req)), Default::default());
1011 vec![a, h]
1012 }),
1013 ("hessenberg", |par| {
1014 let n = 160;
1015 let mut a = rand_mat(n, n, 14);
1016 let bs = 8;
1017 let mut h = Mat::<f64>::zeros(bs, n - 1);
1018 let req = linalg::evd::hessenberg::hessenberg_in_place_scratch::<f64>(n, bs, par.shape(), Default::default());
1019 linalg::evd::hessenberg::hessenberg_in_place(a.as_mut(), h.as_mut(), par, MemStack::new(&mut MemBuffer::new(req)), Default::default());
1020 vec![a, h]
1021 }),
1022 ("tridiagonalization", |par| {
1023 let n = 200;
1024 let x = rand_mat(n, n, 15);
1025 let mut a = &x + x.transpose();
1026 let bs = 8;
1027 let mut h = Mat::<f64>::zeros(bs, n - 1);
1028 let req = linalg::evd::tridiag::tridiag_in_place_scratch::<f64>(n, par.shape(), Default::default());
1029 linalg::evd::tridiag::tridiag_in_place(a.as_mut(), h.as_mut(), par, MemStack::new(&mut MemBuffer::new(req)), Default::default());
1030 vec![a, h]
1031 }),
1032 ("bidiagonalization", |par| {
1033 let (m, n) = (300, 200);
1034 let mut a = rand_mat(m, n, 16);
1035 let bs = 8;
1036 let mut hl = Mat::<f64>::zeros(bs, n);
1037 let mut hr = Mat::<f64>::zeros(bs, n - 1);
1038 let req = linalg::svd::bidiag::bidiag_in_place_scratch::<f64>(m, n, par.shape(), Default::default());
1039 linalg::svd::bidiag::bidiag_in_place(a.as_mut(), hl.as_mut(), hr.as_mut(), par, MemStack::new(&mut MemBuffer::new(req)), Default::default());
1040 vec![a, hl, hr]
1041 }),
1042 ("bunch kaufman, full pivoting", |par| {
1043 use linalg::cholesky::lblt::factor::{LbltParams, PivotingStrategy};
1044 let n = 200;
1045 let x = rand_mat(n, n, 17);
1046 let mut a = &x + x.transpose();
1047 let mut sub = crate::diag::Diag::<f64>::zeros(n);
1048 let (mut p, mut pi) = (vec![0usize; n], vec![0usize; n]);
1049 let params = LbltParams {
1050 pivoting: PivotingStrategy::Full,
1051 ..<LbltParams as crate::Auto<f64>>::auto()
1052 };
1053 let req = linalg::cholesky::lblt::factor::cholesky_in_place_scratch::<usize, f64>(n, par.shape(), params.into());
1054 linalg::cholesky::lblt::factor::cholesky_in_place(a.as_mut(), sub.as_mut(), &mut p, &mut pi, par, MemStack::new(&mut MemBuffer::new(req)), params.into());
1055 vec![a, sub.column_vector().as_mat().to_owned()]
1056 }),
1057 ("triangular solve", |par| {
1058 let (n, k) = (100, 512);
1059 let l = rand_mat(n, n, 18) + Mat::<f64>::identity(n, n) * (n as f64);
1060 let mut x = rand_mat(n, k, 19);
1061 linalg::triangular_solve::solve_lower_triangular_in_place(l.as_ref(), x.as_mut(), par);
1062 vec![x]
1063 }),
1064 ("matvec, row split", |par| {
1065 let a = rand_mat(1000, 1000, 20);
1066 let v = rand_mat(1000, 1, 21);
1067 let mut y = Mat::<f64>::zeros(1000, 1);
1068 matmul(y.as_mut(), Accum::Replace, a.as_ref(), v.as_ref(), 1.0, par);
1069 vec![y]
1070 }),
1071 ("matvec, column split", |par| {
1072 let a = rand_mat(1000, 1000, 22);
1073 let v = rand_mat(1000, 1, 23);
1074 let mut y = Mat::<f64>::zeros(1, 1000);
1075 matmul(y.as_mut(), Accum::Replace, v.transpose(), a.as_ref(), 1.0, par);
1076 vec![y]
1077 }),
1078 ("block householder apply", |par| {
1079 let (m, b, k) = (2000, 8, 256);
1080 let basis = rand_mat(m, b, 24);
1081 let factor = Mat::<f64>::from_fn(b, b, |i, j| if i == j { 1.5 } else if i < j { 0.1 } else { 0.0 });
1082 let mut target = rand_mat(m, k, 25);
1083 let req = linalg::householder::apply_block_householder_on_the_left_in_place_scratch::<f64>(m, b, k);
1084 linalg::householder::apply_block_householder_on_the_left_in_place_with_conj(
1085 basis.as_ref(), factor.as_ref(), crate::Conj::No, target.as_mut(), par, MemStack::new(&mut MemBuffer::new(req)),
1086 );
1087 vec![target]
1088 }),
1089 ("householder factor upgrade", |par| {
1090 let (rows, b) = (4000, 16);
1091 let essentials = rand_mat(rows, b, 26);
1092 let mut factor = Mat::<f64>::from_fn(b, b, |i, j| if i == j { 1.25 } else { 0.0 });
1093 linalg::householder::upgrade_householder_factor(factor.as_mut(), essentials.as_ref(), b, 1, par);
1094 vec![factor]
1095 }),
1096 ("sparse product", |par| {
1097 use crate::sparse::{SparseColMat, Triplet};
1098 let n = 3000;
1099 let triplets: Vec<_> = (0..n)
1100 .flat_map(|j| (0..4).map(move |t| Triplet::new((j * 7 + t * 131) % n, j, ((j + t) as f64).sin())))
1101 .collect();
1102 let a = SparseColMat::<usize, f64>::try_new_from_triplets(n, n, &triplets).unwrap();
1103 let c = crate::sparse::linalg::matmul::sparse_sparse_matmul(a.as_ref(), a.as_ref(), 1.0, par).unwrap();
1104 vec![c.to_dense()]
1105 }),
1106 ("tiled product (the non-x86 route)", |par| {
1107 let (m, n, k) = (301, 263, 190);
1108 let a = rand_mat(m, k, 27);
1109 let b = rand_mat(k, n, 28);
1110 let mut c = Mat::<f64>::zeros(m, n);
1111 match par {
1112 Par::Team(team) => unsafe {
1113 gemm_tiled::<f64>(
1114 team, m, n, k, 1.0, c.as_ptr_mut(), c.col_stride(), c.row_stride(), false,
1115 a.as_ptr(), a.col_stride(), a.row_stride(), b.as_ptr(), b.col_stride(), b.row_stride(),
1116 0.0, 1.0, false, false,
1117 )
1118 },
1119 Par::Seq => matmul(c.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, Par::Seq),
1120 }
1121 vec![c]
1122 }),
1123 ];
1124 for (name, run) in families {
1125 let want = run(Par::Seq);
1126 let (got, s) = with_row(8, &row, |par| {
1127 stats::enable(true);
1128 let _ = stats::take();
1129 let got = run(par);
1130 stats::enable(false);
1131 (got, stats::take())
1132 });
1133 assert!(s.width[4] > 0, "{name}: no region narrowed to 4 ({s:?})");
1135 assert!(s.split[4] + s.run[4] > 0, "{name}: no split or section of 4 ({s:?})");
1138 assert!(s.split.iter().skip(5).all(|&c| c == 0), "{name}: a split wider than 4 ({s:?})");
1139 for (k, (a, b)) in want.iter().zip(got.iter()).enumerate() {
1140 tol(a.as_ref(), b.as_ref(), &alloc::format!("{name} output {k}"));
1141 }
1142 }
1143 }
1144
1145 #[test]
1149 fn never_reaches_the_compact_factor_upgrades() {
1150 use crate::dyn_stack::{MemBuffer, MemStack};
1151 use crate::linalg;
1152 use crate::utils::cost::ParThreshold;
1153 let ns = StdDuration::from_nanos;
1154 let row = [(2, ns(10)), (4, ns(10)), (5, StdDuration::from_secs(1))];
1155 let s = with_row(8, &row, |mut par| {
1156 stats::enable(true);
1157 let _ = stats::take();
1158 {
1159 use linalg::qr::col_pivoting::factor::*;
1160 let (m, n) = (33, 9);
1161 let mut a = rand_mat(m, n, 31);
1162 let mut h = Mat::<f64>::zeros(n, n);
1163 let (mut q, mut qi) = (vec![0usize; n], vec![0usize; n]);
1164 let params = ColPivQrParams { par_threshold: ParThreshold::Never, ..<ColPivQrParams as crate::Auto<f64>>::auto() };
1165 let req = qr_in_place_scratch::<usize, f64>(m, n, n, par.shape(), params.into());
1166 qr_in_place(a.as_mut(), h.as_mut(), &mut q, &mut qi, par.reborrow(), MemStack::new(&mut MemBuffer::new(req)), params.into());
1167 }
1168 {
1169 use linalg::svd::bidiag::*;
1170 let (m, n) = (300, 200);
1171 let mut a = rand_mat(m, n, 32);
1172 let mut hl = Mat::<f64>::zeros(8, n);
1173 let mut hr = Mat::<f64>::zeros(8, n - 1);
1174 let params = BidiagParams { par_threshold: ParThreshold::Never, ..<BidiagParams as crate::Auto<f64>>::auto() };
1175 let req = bidiag_in_place_scratch::<f64>(m, n, par.shape(), params.into());
1176 bidiag_in_place(a.as_mut(), hl.as_mut(), hr.as_mut(), par, MemStack::new(&mut MemBuffer::new(req)), params.into());
1177 }
1178 stats::enable(false);
1179 stats::take()
1180 });
1181 let parallel = s.run.iter().skip(2).sum::<usize>() + s.split.iter().skip(2).sum::<usize>() + s.gemm_tiled + s.gemm_team_kernel;
1182 assert_eq!(parallel, 0, "{s:?}");
1183 }
1184
1185 #[test]
1190 fn for_each_child_deals_every_item_once() {
1191 use rayon::prelude::*;
1192 for (width, len, n_jobs, big) in [
1193 (8usize, 1000usize, 7usize, false),
1194 (8, 1000, 7, true),
1195 (70, 1000, 70, false),
1196 (70, 333, 69, true),
1197 (5, 3, 5, false),
1198 ] {
1199 with_par(width, |par| {
1200 let Par::Team(team) = par else { unreachable!() };
1201 let seen: Vec<AtomicUsize> = (0..len).map(|_| AtomicUsize::new(0)).collect();
1202 let widths: Vec<AtomicUsize> = (0..len).map(|_| AtomicUsize::new(0)).collect();
1203 let f = |i: usize, child: TeamExec<'_>| {
1204 seen[i].fetch_add(1, Ordering::Relaxed);
1205 widths[i].store(child.size().get(), Ordering::Relaxed);
1206 };
1207 if big {
1208 core::assert!(core::mem::size_of::<[u8; 512]>() > FLAT_BYTES);
1209 for_each_child(team, n_jobs, (0..len).into_par_iter().map_with([0u8; 512], |_, i| i), f);
1210 } else {
1211 for_each_child(team, n_jobs, (0..len).into_par_iter(), f);
1212 }
1213 let jobs = n_jobs.min(len).min(width);
1214 let (lo, hi) = (width / jobs, width.div_ceil(jobs));
1215 for i in 0..len {
1216 core::assert_eq!(seen[i].load(Ordering::Relaxed), 1, "{width} {len} {n_jobs} {big}: item {i}");
1217 let w = widths[i].load(Ordering::Relaxed);
1218 core::assert!(lo <= w && w <= hi, "{width} {len} {n_jobs} {big}: item {i} on a child of {w}");
1219 }
1220 });
1221 }
1222 }
1223
1224 #[test]
1227 fn for_each_child_unwinds_and_keeps_the_team() {
1228 use rayon::prelude::*;
1229 use std::panic::{catch_unwind, AssertUnwindSafe};
1230 struct Counted<'a>(&'a AtomicUsize);
1231 impl Drop for Counted<'_> {
1232 fn drop(&mut self) {
1233 self.0.fetch_add(1, Ordering::Relaxed);
1234 }
1235 }
1236 for width in [8usize, 70] {
1237 with_par(width, |mut par| {
1238 let dropped = AtomicUsize::new(0);
1239 let made = AtomicUsize::new(0);
1240 let r = catch_unwind(AssertUnwindSafe(|| {
1241 let Par::Team(team) = par.reborrow() else { unreachable!() };
1242 for_each_child(
1243 team,
1244 width,
1245 (0..1000usize).into_par_iter().map(|i| {
1246 made.fetch_add(1, Ordering::Relaxed);
1247 (i, Counted(&dropped))
1248 }),
1249 |(i, _c), _| {
1250 if i == 500 {
1251 panic!("block failure");
1252 }
1253 },
1254 );
1255 }));
1256 core::assert!(r.is_err());
1257 core::assert_eq!(dropped.load(Ordering::Relaxed), made.load(Ordering::Relaxed));
1258 let Par::Team(team) = par.reborrow() else { unreachable!() };
1260 let hits = AtomicUsize::new(0);
1261 for_each_child(team, width, (0..100usize).into_par_iter(), |_, _| {
1262 hits.fetch_add(1, Ordering::Relaxed);
1263 });
1264 core::assert_eq!(hits.load(Ordering::Relaxed), 100);
1265 });
1266 }
1267 }
1268
1269 #[test]
1271 fn par_pays_time_is_the_shape_gate() {
1272 use crate::utils::cost::par_pays_time;
1273 use core::time::Duration;
1274 with_par(6, |par| {
1275 let shape = par.shape();
1276 for &us in &[0u64, 1, 3, 5, 8, 12, 30, 1000] {
1277 for sections in 1..=3 {
1278 for width in 1..=8usize {
1279 let est = Duration::from_micros(us);
1280 let w = core::num::NonZeroUsize::new(width.min(6)).unwrap();
1281 let want = width >= 2 && shape.pays(est, sections, w);
1282 assert_eq!(par_pays_time(&par, est, sections, width), want, "{us} us, {sections}, {width}");
1283 }
1284 }
1285 }
1286 });
1287 assert!(!par_pays_time(&Par::Seq, Duration::from_secs(1), 1, 4));
1288 }
1289
1290 #[test]
1291 fn grid_uses_the_team_and_stays_in_bounds() {
1292 for nt in 1..=12 {
1293 for &(m, n) in &[(2048, 2048), (4096, 64), (64, 4096), (100, 7), (8, 4), (1, 1000)] {
1294 let (pm, pn) = tile_grid(m, n, nt);
1295 assert!(pm * pn <= nt && pm >= 1 && pn >= 1, "{m}x{n} nt={nt}");
1296 let mut rows = 0;
1298 for i in 0..pm {
1299 let (a, b) = grain_range(m, pm, i, ROW_GRAIN);
1300 assert_eq!(a, rows);
1301 rows = b;
1302 }
1303 assert_eq!(rows, m);
1304 let mut cols = 0;
1305 for j in 0..pn {
1306 let (a, b) = grain_range(n, pn, j, COL_GRAIN);
1307 assert_eq!(a, cols);
1308 cols = b;
1309 }
1310 assert_eq!(cols, n);
1311 }
1312 }
1313 assert_eq!(tile_grid(2048, 2048, 6).0 * tile_grid(2048, 2048, 6).1, 6);
1314 }
1315
1316 #[test]
1317 fn gemm_on_the_team_matches_sequential() {
1318 with_par(6, |mut par| {
1319 for &(m, n, k) in &[(512, 512, 512), (257, 301, 129), (4096, 64, 64), (64, 4096, 64), (1000, 33, 500)] {
1320 let a = rand_mat(m, k, 1);
1321 let b = rand_mat(k, n, 2);
1322 for beta in [Accum::Replace, Accum::Add] {
1323 let mut want = rand_mat(m, n, 3);
1324 let mut got = want.clone();
1325 matmul(want.as_mut(), beta, a.as_ref(), b.as_ref(), 1.5, Par::Seq);
1326 matmul(got.as_mut(), beta, a.as_ref(), b.as_ref(), 1.5, par.reborrow());
1327 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-10, "{m}x{n}x{k}");
1328 }
1329 let mut want = Mat::<f64>::zeros(m, n);
1331 let mut got = Mat::<f64>::zeros(m, n);
1332 let at = a.transpose().to_owned();
1333 matmul(want.as_mut(), Accum::Replace, at.transpose(), b.as_ref(), 1.0, Par::Seq);
1334 matmul(got.as_mut(), Accum::Replace, at.transpose(), b.as_ref(), 1.0, par.reborrow());
1335 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-10);
1336 }
1337 let (m, n, k) = (300, 280, 200);
1339 let a = Mat::<c64>::from_fn(m, k, |i, j| c64::new(i as f64 * 0.01 - j as f64 * 0.003, (i + j) as f64 * 0.002));
1340 let b = Mat::<c64>::from_fn(k, n, |i, j| c64::new((i * j % 7) as f64 * 0.1, i as f64 * 0.01 - j as f64 * 0.01));
1341 let mut want = Mat::<c64>::zeros(m, n);
1342 let mut got = Mat::<c64>::zeros(m, n);
1343 matmul(want.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), c64::new(1.0, 0.0), Par::Seq);
1344 matmul(got.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), c64::new(1.0, 0.0), par.reborrow());
1345 let mut d: f64 = 0.0;
1346 for j in 0..n {
1347 for i in 0..m {
1348 d = d.max((want[(i, j)] - got[(i, j)]).norm_sqr().sqrt());
1349 }
1350 }
1351 assert!(d < 1e-9);
1352 });
1353 }
1354
1355 #[test]
1359 fn the_team_floor_is_per_dtype() {
1360 let engaged = |st: &stats::Stats| st.gemm_tiled + st.gemm_team_kernel;
1362 let before = |m: usize, n: usize, k: usize, par: &mut Par<'_>, cplx: bool| {
1363 stats::enable(true);
1364 let _ = stats::take();
1365 if cplx {
1366 let a = Mat::<c64>::from_fn(m, k, |i, j| c64::new((i + j) as f64, 1.0));
1367 let b = Mat::<c64>::from_fn(k, n, |i, j| c64::new(i as f64, j as f64));
1368 let mut d = Mat::<c64>::zeros(m, n);
1369 matmul(d.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), c64::new(1.0, 0.0), par.reborrow());
1370 } else {
1371 let a = rand_mat(m, k, 1);
1372 let b = rand_mat(k, n, 2);
1373 let mut d = Mat::<f64>::zeros(m, n);
1374 matmul(d.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, par.reborrow());
1375 }
1376 let st = stats::take();
1377 stats::enable(false);
1378 st
1379 };
1380 with_par(6, |mut par| {
1381 let st = before(64, 64, 64, &mut par, true);
1383 assert_eq!(st.gemm_below_floor, 0, "{st:?}");
1384 assert_eq!(engaged(&st), 1, "{st:?}");
1385 let st = before(20, 20, 20, &mut par, false);
1387 assert_eq!(engaged(&st), 0, "{st:?}");
1388 let st = before(192, 192, 192, &mut par, false);
1390 assert_eq!(engaged(&st), 1, "{st:?}");
1391 });
1392 }
1393
1394 #[test]
1395 fn teams_of_odd_and_unit_width_match_sequential() {
1396 let (m, n, k) = (300, 200, 250);
1397 let a = rand_mat(m, k, 1);
1398 let b = rand_mat(k, n, 2);
1399 let mut want = Mat::<f64>::zeros(m, n);
1400 matmul(want.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, Par::Seq);
1401 for width in [1usize, 3, 5] {
1402 let mut got = Mat::<f64>::zeros(m, n);
1403 with_par(width, |par| {
1404 matmul(got.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, par)
1405 });
1406 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-12, "width = {width}");
1407 }
1408 }
1409
1410 #[test]
1411 fn for_each_deals_every_item_once_and_nests() {
1412 with_par(6, |mut par| {
1413 for jobs in [1usize, 2, 3, 6, 9] {
1414 let hits: Vec<AtomicUsize> = (0..13).map(|_| AtomicUsize::new(0)).collect();
1415 crate::utils::thread::for_each_with_par(
1416 par.reborrow(),
1417 jobs,
1418 rayon::iter::IntoParallelIterator::into_par_iter(0..13usize),
1419 |i, child| {
1420 hits[i].fetch_add(1, Ordering::Relaxed);
1421 assert!(child.degree() <= 6);
1423 },
1424 );
1425 assert!(hits.iter().all(|h| h.load(Ordering::Relaxed) == 1), "jobs = {jobs}");
1426 }
1427 });
1428 }
1429
1430 #[test]
1431 fn join_raw_leaves_match_the_width() {
1432 for n in [2usize, 3, 5, 6] {
1433 let widths = std::sync::Mutex::new(Vec::new());
1434 with_par(n, |par| {
1435 crate::utils::thread::join_raw(
1436 |p| widths.lock().unwrap().push(p.degree()),
1437 |p| widths.lock().unwrap().push(p.degree()),
1438 par,
1439 )
1440 });
1441 let mut w = widths.into_inner().unwrap();
1442 w.sort_unstable();
1443 let half = n / 2;
1444 assert_eq!(w, vec![half, n - half], "n = {n}");
1445 }
1446 }
1447
1448 #[test]
1449 fn lu_qr_cholesky_on_the_team_match_sequential() {
1450 use crate::linalg::solvers::{Llt, Qr};
1451 use crate::Side;
1452 with_par(6, |mut par| {
1453 for n in [200usize, 513] {
1454 let a = rand_mat(n, n, 7);
1455 let b = rand_mat(n, 5, 8);
1456 let lu = |par: Par| {
1458 let mut f = a.clone();
1459 let mut perm = vec![0usize; n];
1460 let mut perm_inv = vec![0usize; n];
1461 let mut mem = dyn_stack::MemBuffer::new(
1462 crate::linalg::lu::partial_pivoting::factor::lu_in_place_scratch::<usize, f64>(n, n, par.shape(), Default::default()),
1463 );
1464 crate::linalg::lu::partial_pivoting::factor::lu_in_place(
1465 f.as_mut(),
1466 &mut perm,
1467 &mut perm_inv,
1468 par,
1469 dyn_stack::MemStack::new(&mut mem),
1470 Default::default(),
1471 );
1472 f
1473 };
1474 let want = lu(Par::Seq);
1475 let got = lu(par.reborrow());
1476 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-8, "lu n = {n}");
1477 let mut r_want = a.clone();
1479 let mut r_got = a.clone();
1480 let qr = |m: &mut Mat<f64>, par: Par| {
1481 let bs = crate::linalg::qr::no_pivoting::factor::recommended_block_size::<f64>(n, n);
1482 let mut h = Mat::<f64>::zeros(bs, n);
1483 let mut mem = dyn_stack::MemBuffer::new(
1484 crate::linalg::qr::no_pivoting::factor::qr_in_place_scratch::<f64>(n, n, bs, par.shape(), Default::default()),
1485 );
1486 crate::linalg::qr::no_pivoting::factor::qr_in_place(
1487 m.as_mut(),
1488 h.as_mut(),
1489 par,
1490 dyn_stack::MemStack::new(&mut mem),
1491 Default::default(),
1492 );
1493 };
1494 qr(&mut r_want, Par::Seq);
1495 qr(&mut r_got, par.reborrow());
1496 assert!(max_diff(r_want.as_ref(), r_got.as_ref()) < 1e-8, "qr n = {n}");
1497 let _ = (&b, Side::Lower);
1498 let _: Option<(Llt<f64>, Qr<f64>)> = None;
1499 let spd = {
1501 let mut s = Mat::<f64>::zeros(n, n);
1502 matmul(s.as_mut(), Accum::Replace, a.transpose(), a.as_ref(), 1.0, Par::Seq);
1503 for i in 0..n {
1504 s[(i, i)] += n as f64;
1505 }
1506 s
1507 };
1508 let ch = |par: Par| {
1509 let mut f = spd.clone();
1510 let mut mem = dyn_stack::MemBuffer::new(
1511 crate::linalg::cholesky::llt::factor::cholesky_in_place_scratch::<f64>(n, par.shape(), Default::default()),
1512 );
1513 crate::linalg::cholesky::llt::factor::cholesky_in_place(
1514 f.as_mut(),
1515 Default::default(),
1516 par,
1517 dyn_stack::MemStack::new(&mut mem),
1518 Default::default(),
1519 )
1520 .unwrap();
1521 f
1522 };
1523 let want = ch(Par::Seq);
1524 let got = ch(par.reborrow());
1525 let mut d: f64 = 0.0;
1527 for j in 0..n {
1528 for i in j..n {
1529 d = d.max((want[(i, j)] - got[(i, j)]).abs());
1530 }
1531 }
1532 assert!(d < 1e-8, "cholesky n = {n}: {d}");
1533 }
1534 });
1535 }
1536
1537 fn counted<R>(width: usize, f: impl FnOnce(Par<'_>) -> R) -> (R, stats::Stats) {
1539 with_par(width, |par| {
1540 stats::enable(true);
1541 let _ = stats::take();
1542 let r = f(par);
1543 let st = stats::take();
1544 stats::enable(false);
1545 (r, st)
1546 })
1547 }
1548
1549 #[test]
1550 fn sparse_matmul_on_a_team_runs_on_the_team() {
1551 use crate::sparse::{SparseColMat, Triplet};
1552 let n = 3000;
1553 let triplets: Vec<_> = (0..n)
1554 .flat_map(|j| {
1555 (0..4).map(move |t| {
1556 let i = (j * 7 + t * 131) % n;
1557 Triplet::new(i, j, ((i + 2 * j) as f64).sin())
1558 })
1559 })
1560 .collect();
1561 let a = SparseColMat::<usize, f64>::try_new_from_triplets(n, n, &triplets).unwrap();
1562 let want = crate::sparse::linalg::matmul::sparse_sparse_matmul(
1563 a.as_ref(), a.as_ref(), 1.0, Par::Seq,
1564 )
1565 .unwrap();
1566 let (got, st) = counted(4, |par| {
1567 crate::sparse::linalg::matmul::sparse_sparse_matmul(a.as_ref(), a.as_ref(), 1.0, par).unwrap()
1568 });
1569 assert!(st.split.get(4).copied().unwrap_or(0) >= 1, "{:?}", st.split);
1571 assert!(max_diff(want.to_dense().as_ref(), got.to_dense().as_ref()) < 1e-12);
1572 }
1573
1574 #[test]
1575 fn lu_swap_stage_splits_the_team_it_is_handed() {
1576 let n = 513;
1577 let a = rand_mat(n, n, 11);
1578 let lu = |par: Par| {
1579 let mut f = a.clone();
1580 let mut perm = vec![0usize; n];
1581 let mut perm_inv = vec![0usize; n];
1582 let mut mem = dyn_stack::MemBuffer::new(
1583 crate::linalg::lu::partial_pivoting::factor::lu_in_place_scratch::<usize, f64>(
1584 n, n, par.shape(), Default::default(),
1585 ),
1586 );
1587 crate::linalg::lu::partial_pivoting::factor::lu_in_place(
1588 f.as_mut(), &mut perm, &mut perm_inv, par,
1589 dyn_stack::MemStack::new(&mut mem), Default::default(),
1590 );
1591 f
1592 };
1593 let want = lu(Par::Seq);
1594 let (got, st) = counted(6, lu);
1595 assert!(st.split.get(2).copied().unwrap_or(0) >= 1, "{:?}", st.split);
1597 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-8);
1598 }
1599
1600 #[test]
1601 fn join_sized_on_a_team_hands_out_exactly_the_team() {
1602 use rayon::iter::IntoParallelIterator;
1603 for left in 0..=7usize {
1604 let active = AtomicUsize::new(0);
1605 let peak = AtomicUsize::new(0);
1606 let widths = [AtomicUsize::new(0), AtomicUsize::new(0)];
1607 with_par(6, |par| {
1608 let side = |which: usize, par: Par<'_>| {
1609 let jobs = par.degree();
1610 widths[which].store(jobs, Ordering::Relaxed);
1611 crate::utils::thread::for_each_with_par(par, jobs, (0..jobs).into_par_iter(), |_, _| {
1612 let now = active.fetch_add(1, Ordering::AcqRel) + 1;
1613 peak.fetch_max(now, Ordering::AcqRel);
1614 std::thread::sleep(std::time::Duration::from_millis(3));
1615 active.fetch_sub(1, Ordering::AcqRel);
1616 });
1617 };
1618 crate::utils::thread::join_sized(par, left, |p| side(0, p), |p| side(1, p));
1619 });
1620 assert_eq!(
1621 widths[0].load(Ordering::Relaxed) + widths[1].load(Ordering::Relaxed),
1622 6,
1623 "left = {left}"
1624 );
1625 assert!(peak.load(Ordering::Relaxed) <= 6, "left = {left}");
1626 }
1627 }
1628 #[test]
1629 fn a_team_of_one_behaves_exactly_like_seq() {
1630 use crate::utils::cost::{par_width, rate, ParThreshold};
1631 let _turn = team_turn();
1633 threadteam::Exec::serial()
1634 .team(|team| {
1635 let par = Par::Team(team);
1636 assert_eq!(par.degree(), 1);
1637 assert_eq!(par_width::<f64>(&par, 1 << 40, rate::gemm_mac(), 1), 1);
1639 assert!(!crate::utils::cost::join_pays::<f64>(&par, 1 << 40, rate::gemm_mac(), 1));
1640 assert_eq!(ParThreshold::Derived.region_width::<f64>(&par, 1 << 40, rate::gemm_mac(), 1), 1);
1641 let a = rand_mat(200, 150, 1);
1643 let b = rand_mat(150, 180, 2);
1644 let mut want = Mat::<f64>::zeros(200, 180);
1645 let mut got = Mat::<f64>::zeros(200, 180);
1646 matmul(want.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, Par::Seq);
1647 stats::enable(true);
1648 let _ = stats::take();
1649 matmul(got.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, par);
1650 let st = stats::take();
1651 stats::enable(false);
1652 assert_eq!(max_diff(want.as_ref(), got.as_ref()), 0.0);
1653 assert_eq!(st.gemm_team_kernel + st.gemm_tiled, 0, "{st:?}");
1654 })
1655 .unwrap();
1656 }
1657
1658 #[test]
1662 fn triangular_products_on_the_team_match_sequential_and_stay_on_it() {
1663 use crate::linalg::matmul::triangular::{matmul as tri_matmul, BlockStructure};
1664 let (n, k) = (768, 768);
1665 let a = rand_mat(n, k, 5);
1666 let b = rand_mat(k, n, 6);
1667 let lower = |par: Par| {
1668 let mut c = rand_mat(n, n, 7);
1669 tri_matmul(c.as_mut(), BlockStructure::TriangularLower, Accum::Add, a.as_ref(), BlockStructure::Rectangular, b.as_ref(), BlockStructure::Rectangular, 0.5, par);
1670 c
1671 };
1672 let upper = |par: Par| {
1673 let mut c = rand_mat(n, n, 8);
1674 tri_matmul(c.as_mut(), BlockStructure::TriangularUpper, Accum::Replace, a.as_ref(), BlockStructure::Rectangular, b.as_ref(), BlockStructure::Rectangular, 1.0, par);
1675 c
1676 };
1677 let (want_l, want_u) = (lower(Par::Seq), upper(Par::Seq));
1678 let ((got_l, got_u), st) = counted(6, |mut par| (lower(par.reborrow()), upper(par.reborrow())));
1679 let tri = |m: &Mat<f64>, w: &Mat<f64>, lo: bool| {
1680 let mut d: f64 = 0.0;
1681 for j in 0..n {
1682 for i in 0..n {
1683 if (lo && i >= j) || (!lo && i <= j) {
1684 d = d.max((m[(i, j)] - w[(i, j)]).abs());
1685 }
1686 }
1687 }
1688 d
1689 };
1690 assert!(tri(&got_l, &want_l, true) < 1e-9);
1691 assert!(tri(&got_u, &want_u, false) < 1e-9);
1692 assert!(st.gemm_tiled > 0 || st.gemm_team_kernel > 0 || st.split.iter().skip(2).any(|&c| c > 0), "{st:?}");
1694 }
1695
1696 #[test]
1699 fn spicy_matmul_on_a_team_matches_sequential() {
1700 use crate::linalg::matmul::internal::spicy_matmul;
1701 use crate::linalg::matmul::triangular::BlockStructure;
1702 if !crate::linalg::matmul::internal::has_spicy_matmul::<f64>() {
1703 return;
1704 }
1705 let (n, k) = (768, 512);
1706 let a = rand_mat(n, k, 9);
1707 let b = rand_mat(k, n, 10);
1708 let run = |par: Par| {
1709 let mut c = rand_mat(n, n, 11);
1710 spicy_matmul::<usize, f64>(c.as_mut(), BlockStructure::TriangularLower, None, None, Accum::Add, a.as_ref(), crate::Conj::No, b.as_ref(), crate::Conj::No, None, 0.5, par, dyn_stack::MemStack::new(&mut []));
1711 c
1712 };
1713 let want = run(Par::Seq);
1714 let got = with_par(6, |par| run(par));
1715 let mut d: f64 = 0.0;
1716 for j in 0..n {
1717 for i in j..n {
1718 d = d.max((want[(i, j)] - got[(i, j)]).abs());
1719 }
1720 }
1721 assert!(d < 1e-9, "{d}");
1722 }
1723
1724 #[test]
1727 fn gemm_tiled_matches_the_gemm_crate_for_every_dtype() {
1728 use num_complex::Complex;
1729 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1730 assert_eq!(
1731 x86_kernel::<f64>().is_some(),
1732 std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma")
1733 );
1734 fn check<E: 'static + Copy + Send + Sync + core::fmt::Debug>(
1735 par: &mut Par<'_>,
1736 one: E,
1737 from: impl Fn(f64) -> E,
1738 err: impl Fn(E, E) -> f64,
1739 cplx: bool,
1740 ) {
1741 let (m, n, k) = (301, 263, 190);
1742 let gen = |len: usize, seed: u64| -> Vec<E> {
1743 let mut x = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1744 (0..len)
1745 .map(|_| {
1746 x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1747 from(((x >> 11) as f64 / (1u64 << 53) as f64) - 0.5)
1748 })
1749 .collect()
1750 };
1751 let (a, b) = (gen(m * k, 1), gen(k * n, 2));
1752 let zero = from(0.0);
1753 let alpha = from(1.25);
1754 for read_dst in [false, true] {
1755 for conj in [false, true] {
1756 if conj && !cplx {
1757 continue;
1758 }
1759 let base = gen(m * n, 3);
1760 let (mut want, mut got) = (base.clone(), base);
1761 let acc = if read_dst { one } else { zero };
1762 unsafe {
1763 gemm::gemm(
1764 m, n, k, want.as_mut_ptr(), m as isize, 1, read_dst,
1765 a.as_ptr(), m as isize, 1, b.as_ptr(), k as isize, 1,
1766 acc, alpha, false, conj, conj, gemm::Parallelism::None,
1767 );
1768 let Par::Team(team) = par.reborrow() else { unreachable!() };
1769 gemm_tiled::<E>(
1770 team, m, n, k, 1e3, got.as_mut_ptr(), m as isize, 1, read_dst,
1771 a.as_ptr(), m as isize, 1, b.as_ptr(), k as isize, 1,
1772 acc, alpha, conj, conj,
1773 );
1774 }
1775 let d = want.iter().zip(&got).map(|(&w, &g)| err(w, g)).fold(0.0, f64::max);
1776 assert!(d < 1e-3, "read_dst {read_dst} conj {conj}: {d}");
1777 }
1778 }
1779 }
1780 with_par(5, |mut par| {
1781 check::<f64>(&mut par, 1.0, |x| x, |w, g| (w - g).abs(), false);
1782 check::<f32>(&mut par, 1.0, |x| x as f32, |w, g| (w - g).abs() as f64, false);
1783 check::<Complex<f64>>(&mut par, Complex::new(1.0, 0.0), |x| Complex::new(x, 0.5 - x), |w, g| (w - g).norm(), true);
1784 check::<Complex<f32>>(&mut par, Complex::new(1.0, 0.0), |x| Complex::new(x as f32, 0.5 - x as f32), |w, g| (w - g).norm() as f64, true);
1785 });
1786 let (_, st) = counted(5, |mut par| check::<f64>(&mut par, 1.0, |x| x, |w, g| (w - g).abs(), false));
1787 assert!(st.gemm_tiled >= 2 && st.gemm_below_floor == 0, "{st:?}");
1788 }
1789
1790 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1792 struct Recording<'a> {
1793 inner: &'a dyn private_gemm_x86::Fork,
1794 seen: std::sync::Mutex<Vec<std::thread::ThreadId>>,
1795 runs: AtomicUsize,
1796 }
1797
1798 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1799 impl private_gemm_x86::Fork for Recording<'_> {
1800 fn width(&self) -> usize {
1801 self.inner.width()
1802 }
1803 fn run(&self, f: &(dyn Fn(usize) + Sync)) {
1804 self.runs.fetch_add(1, Ordering::Relaxed);
1805 let seen = &self.seen;
1806 self.inner.run(&|j| {
1807 seen.lock().unwrap().push(std::thread::current().id());
1808 f(j)
1809 });
1810 }
1811 }
1812
1813 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1818 #[test]
1819 fn the_kernel_forks_only_on_members_of_the_team() {
1820 use private_gemm_x86::{gemm_with, Accum as KAccum, DType, DstKind, IType, InstrSet};
1821 let Some((feat, _)) = super::kernel::available::<f64>() else { return };
1822 let _ = InstrSet::Avx256;
1823 let (m, n, k) = (400usize, 360usize, 300usize);
1824 let a = rand_mat(m, k, 1);
1825 let b = rand_mat(k, n, 2);
1826 let mut want = Mat::<f64>::zeros(m, n);
1827 matmul(want.as_mut(), Accum::Replace, a.as_ref(), b.as_ref(), 1.0, Par::Seq);
1828 let _turn = team_turn();
1829 let team = team_of(6);
1830 let mut exec = team.exec(6);
1831 exec.team(|mut token| {
1832 let members = std::sync::Mutex::new(Vec::new());
1834 token.reborrow().run(|_, _| members.lock().unwrap().push(std::thread::current().id()));
1835 let members = members.into_inner().unwrap();
1836 assert_eq!(members.len(), 6);
1837 for width in [6usize, 3, 2] {
1838 let mut got = Mat::<f64>::zeros(m, n);
1839 let alpha = 1.0f64;
1840 let rec = std::cell::RefCell::new(None);
1841 super::kernel::with_fork(token.reborrow(), width, |fork| {
1842 assert_eq!(fork.width(), width);
1843 let recording = Recording { inner: fork, seen: Default::default(), runs: AtomicUsize::new(0) };
1844 unsafe {
1845 gemm_with(
1846 DType::F64, IType::U64, feat, m, n, k,
1847 got.as_mut().as_ptr_mut() as *mut (), 1, got.col_stride(),
1848 core::ptr::null(), core::ptr::null(), DstKind::Full, KAccum::Replace,
1849 a.as_ref().as_ptr() as *const (), 1, a.col_stride(), false,
1850 core::ptr::null(), 0,
1851 b.as_ref().as_ptr() as *const (), 1, b.col_stride(), false,
1852 &raw const alpha as *const (),
1853 &recording,
1854 );
1855 }
1856 *rec.borrow_mut() = Some((recording.seen.into_inner().unwrap(), recording.runs.into_inner()));
1857 });
1858 let (seen, runs) = rec.into_inner().unwrap();
1859 assert!(runs >= 1, "width {width}: the kernel never forked");
1860 assert_eq!(seen.len(), runs * width, "width {width}");
1861 assert!(seen.iter().all(|t| members.contains(t)), "width {width}: a participant outside the team");
1862 let distinct: std::collections::HashSet<_> = seen.iter().collect();
1863 assert_eq!(distinct.len(), width, "width {width}: seen {} entries over {runs} runs", seen.len());
1864 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-10, "width {width}");
1865 }
1866 })
1867 .ok()
1868 .expect("a team token");
1869 }
1870
1871 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1874 #[test]
1875 fn team_kernel_products_match_sequential_for_every_dtype() {
1876 use num_complex::Complex;
1877 if super::kernel::available::<f64>().is_none() {
1878 return;
1879 }
1880 fn check<T: crate::ComplexField + Copy>(from: impl Fn(f64, f64) -> T, err: impl Fn(T, T) -> f64) {
1881 let (m, n, k) = (333usize, 287usize, 211usize);
1882 let gen = |r: usize, c: usize, seed: u64| {
1883 let mut x = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1884 Mat::<T>::from_fn(r, c, |_, _| {
1885 x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1886 let u = (x >> 11) as f64 / (1u64 << 53) as f64 - 0.5;
1887 from(u, 0.25 - u)
1888 })
1889 };
1890 let (a, b) = (gen(m, k, 1), gen(k, n, 2));
1891 let alpha = from(0.75, -0.25);
1892 for beta in [Accum::Replace, Accum::Add] {
1893 let base = gen(m, n, 3);
1894 let (mut want, mut got) = (base.clone(), base);
1895 matmul(want.as_mut(), beta, a.as_ref(), b.as_ref(), alpha, Par::Seq);
1896 let ((), st) = counted(6, |par| matmul(got.as_mut(), beta, a.as_ref(), b.as_ref(), alpha, par));
1897 let mut d = 0.0f64;
1898 for j in 0..n {
1899 for i in 0..m {
1900 d = d.max(err(want[(i, j)], got[(i, j)]));
1901 }
1902 }
1903 assert!(d < 1e-3, "{beta:?}: {d}");
1904 assert_eq!(st.gemm_team_kernel, 1, "{st:?}");
1905 assert_eq!(st.gemm_tiled, 0, "{st:?}");
1906 }
1907 }
1908 check::<f64>(|x, _| x, |w, g| (w - g).abs());
1909 check::<f32>(|x, _| x as f32, |w, g| (w - g).abs() as f64);
1910 check::<Complex<f64>>(|x, y| Complex::new(x, y), |w, g| (w - g).norm());
1911 check::<Complex<f32>>(|x, y| Complex::new(x as f32, y as f32), |w, g| (w - g).norm() as f64);
1912 }
1913
1914 #[cfg(all(target_arch = "x86_64", feature = "std"))]
1918 #[test]
1919 fn team_kernel_triangular_indexed_and_factorizations_match_sequential() {
1920 use crate::linalg::matmul::internal::spicy_matmul;
1921 use crate::linalg::matmul::triangular::{matmul as tri_matmul, BlockStructure};
1922 if !crate::linalg::matmul::internal::has_spicy_matmul::<f64>() {
1923 return;
1924 }
1925 let (n, k) = (700usize, 500usize);
1926 let a = rand_mat(n, k, 21);
1927 let b = rand_mat(k, n, 22);
1928 let tri_diff = |x: &Mat<f64>, y: &Mat<f64>, lower: bool| {
1929 let mut d: f64 = 0.0;
1930 for j in 0..n {
1931 for i in 0..n {
1932 if (lower && i >= j) || (!lower && i <= j) {
1933 d = d.max((x[(i, j)] - y[(i, j)]).abs());
1934 }
1935 }
1936 }
1937 d
1938 };
1939 for (block, lower) in [(BlockStructure::TriangularLower, true), (BlockStructure::TriangularUpper, false)] {
1941 let run = |par: Par| {
1942 let mut c = rand_mat(n, n, 23);
1943 spicy_matmul::<usize, f64>(c.as_mut(), block, None, None, Accum::Add, a.as_ref(), crate::Conj::No, b.as_ref(), crate::Conj::No, None, 0.5, par, dyn_stack::MemStack::new(&mut []));
1944 c
1945 };
1946 let want = run(Par::Seq);
1947 let (got, st) = counted(6, run);
1948 assert!(tri_diff(&want, &got, lower) < 1e-9);
1949 assert_eq!(st.gemm_team_kernel, 1, "{st:?}");
1950 }
1951 {
1953 let rows: Vec<usize> = (0..n).map(|i| (i * 3) % (3 * n - 1) + (i % 2)).collect::<std::collections::BTreeSet<_>>().into_iter().take(n).collect();
1954 let cols: Vec<usize> = (0..n).map(|j| 2 * j).collect();
1955 let (nr, nc) = (rows.iter().max().unwrap() + 1, 2 * n);
1956 let m_rows = rows.len();
1957 let a2 = rand_mat(m_rows, k, 24);
1958 let run = |par: Par| {
1959 let mut c = rand_mat(nr, nc, 25);
1960 spicy_matmul::<usize, f64>(c.as_mut(), BlockStructure::Rectangular, Some(&rows), Some(&cols), Accum::Add, a2.as_ref(), crate::Conj::No, b.as_ref(), crate::Conj::No, None, 0.5, par, dyn_stack::MemStack::new(&mut []));
1961 c
1962 };
1963 let want = run(Par::Seq);
1964 let (got, st) = counted(6, run);
1965 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-9);
1966 assert_eq!(st.gemm_team_kernel, 1, "{st:?}");
1967 }
1968 {
1970 let run = |par: Par| {
1971 let mut c = rand_mat(n, n, 26);
1972 tri_matmul(c.as_mut(), BlockStructure::TriangularLower, Accum::Add, a.as_ref(), BlockStructure::Rectangular, b.as_ref(), BlockStructure::Rectangular, 0.5, par);
1973 c
1974 };
1975 let want = run(Par::Seq);
1976 let (got, st) = counted(6, run);
1977 assert!(tri_diff(&want, &got, true) < 1e-9);
1978 assert!(st.gemm_team_kernel >= 1, "{st:?}");
1979 }
1980 let n = 640usize;
1982 let g = rand_mat(n, n, 30);
1983 let spd = {
1984 let mut s = Mat::<f64>::zeros(n, n);
1985 matmul(s.as_mut(), Accum::Replace, g.transpose(), g.as_ref(), 1.0, Par::Seq);
1986 for i in 0..n {
1987 s[(i, i)] += n as f64;
1988 }
1989 s
1990 };
1991 let lower_diff = |x: &Mat<f64>, y: &Mat<f64>| {
1992 let mut d: f64 = 0.0;
1993 for j in 0..n {
1994 for i in j..n {
1995 d = d.max((x[(i, j)] - y[(i, j)]).abs());
1996 }
1997 }
1998 d
1999 };
2000 let llt = |par: Par| {
2001 let mut f = spd.clone();
2002 let mut mem = dyn_stack::MemBuffer::new(crate::linalg::cholesky::llt::factor::cholesky_in_place_scratch::<f64>(n, par.shape(), Default::default()));
2003 crate::linalg::cholesky::llt::factor::cholesky_in_place(f.as_mut(), Default::default(), par, dyn_stack::MemStack::new(&mut mem), Default::default()).unwrap();
2004 f
2005 };
2006 let (got, st) = counted(6, llt);
2007 assert!(lower_diff(&llt(Par::Seq), &got) < 1e-8, "llt");
2008 assert!(st.gemm_team_kernel > 0, "llt: {st:?}");
2009 let ldlt = |par: Par| {
2010 let mut f = spd.clone();
2011 let mut mem = dyn_stack::MemBuffer::new(crate::linalg::cholesky::ldlt::factor::cholesky_in_place_scratch::<f64>(n, par.shape(), Default::default()));
2012 crate::linalg::cholesky::ldlt::factor::cholesky_in_place(
2013 f.as_mut(),
2014 crate::linalg::cholesky::ldlt::factor::LdltRegularization { dynamic_regularization_signs: None, dynamic_regularization_delta: 0.0, dynamic_regularization_epsilon: 0.0 },
2015 par,
2016 dyn_stack::MemStack::new(&mut mem),
2017 Default::default(),
2018 )
2019 .unwrap();
2020 f
2021 };
2022 let (got, st) = counted(6, ldlt);
2023 assert!(lower_diff(&ldlt(Par::Seq), &got) < 1e-8, "ldlt");
2024 assert!(st.gemm_team_kernel > 0, "ldlt: {st:?}");
2025 let qr = |par: Par| {
2026 let mut f = g.clone();
2027 let bs = crate::linalg::qr::no_pivoting::factor::recommended_block_size::<f64>(n, n);
2028 let mut h = Mat::<f64>::zeros(bs, n);
2029 let mut mem = dyn_stack::MemBuffer::new(crate::linalg::qr::no_pivoting::factor::qr_in_place_scratch::<f64>(n, n, bs, par.shape(), Default::default()));
2030 crate::linalg::qr::no_pivoting::factor::qr_in_place(f.as_mut(), h.as_mut(), par, dyn_stack::MemStack::new(&mut mem), Default::default());
2031 f
2032 };
2033 let got = with_par(6, qr);
2036 assert!(max_diff(qr(Par::Seq).as_ref(), got.as_ref()) < 1e-8, "qr");
2037 let lu = |par: Par| {
2038 let mut f = g.clone();
2039 let (mut perm, mut perm_inv) = (vec![0usize; n], vec![0usize; n]);
2040 let mut mem = dyn_stack::MemBuffer::new(crate::linalg::lu::partial_pivoting::factor::lu_in_place_scratch::<usize, f64>(n, n, par.shape(), Default::default()));
2041 crate::linalg::lu::partial_pivoting::factor::lu_in_place(f.as_mut(), &mut perm, &mut perm_inv, par, dyn_stack::MemStack::new(&mut mem), Default::default());
2042 f
2043 };
2044 let (got, st) = counted(6, lu);
2045 assert!(max_diff(lu(Par::Seq).as_ref(), got.as_ref()) < 1e-8, "lu");
2046 assert!(st.gemm_team_kernel > 0, "lu: {st:?}");
2047 }
2048
2049 #[cfg(all(target_arch = "x86_64", feature = "std"))]
2051 #[test]
2052 fn team_kernel_qr_matches_sequential() {
2053 if super::kernel::available::<f64>().is_none() {
2054 return;
2055 }
2056 let n = 1400usize;
2057 let g = rand_mat(n, n, 31);
2058 let qr = |par: Par| {
2059 let mut f = g.clone();
2060 let bs = crate::linalg::qr::no_pivoting::factor::recommended_block_size::<f64>(n, n);
2061 let mut h = Mat::<f64>::zeros(bs, n);
2062 let mut mem = dyn_stack::MemBuffer::new(crate::linalg::qr::no_pivoting::factor::qr_in_place_scratch::<f64>(n, n, bs, par.shape(), Default::default()));
2063 crate::linalg::qr::no_pivoting::factor::qr_in_place(f.as_mut(), h.as_mut(), par, dyn_stack::MemStack::new(&mut mem), Default::default());
2064 f
2065 };
2066 let (got, st) = counted(6, qr);
2067 assert!(max_diff(qr(Par::Seq).as_ref(), got.as_ref()) < 1e-7, "qr");
2068 assert!(st.gemm_team_kernel > 0, "qr: {st:?}");
2069 }
2070
2071 #[test]
2074 fn high_level_singular_values_run_on_the_team() {
2075 let n = 384;
2076 let a = rand_mat(n, n, 21);
2077 let want = a.as_ref().singular_values(Par::Seq).unwrap();
2078 let (got, st) = counted(4, |par| a.as_ref().singular_values(par).unwrap());
2079 assert_eq!(want.len(), got.len());
2080 for (w, g) in want.iter().zip(&got) {
2081 assert!((w - g).abs() < 1e-9 * want[0], "{w} {g}");
2082 }
2083 let opened = st.run.iter().skip(2).sum::<usize>() + st.split.iter().skip(2).sum::<usize>();
2084 assert!(opened > 0 || st.gemm_tiled > 0 || st.gemm_team_kernel > 0, "no team section: {st:?}");
2085 }
2086
2087 #[test]
2089 fn high_level_qr_and_lu_solve_on_the_team() {
2090 use crate::linalg::solvers::Solve;
2091 let n = 320;
2092 let a = rand_mat(n, n, 22);
2093 let b = rand_mat(n, 8, 23);
2094 let want = a.as_ref().partial_piv_lu(Par::Seq).solve(b.as_ref(), Par::Seq);
2095 let (got, st) = counted(4, |mut par| {
2096 let lu = a.as_ref().partial_piv_lu(par.reborrow());
2097 lu.solve(b.as_ref(), par)
2098 });
2099 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-8);
2100 let opened = st.run.iter().skip(2).sum::<usize>() + st.split.iter().skip(2).sum::<usize>();
2101 assert!(opened > 0 || st.gemm_tiled > 0 || st.gemm_team_kernel > 0, "no team section: {st:?}");
2102 let want = a.as_ref().qr(Par::Seq).solve(b.as_ref(), Par::Seq);
2103 let got = with_par(4, |mut par| {
2104 let qr = a.as_ref().qr(par.reborrow());
2105 qr.solve(b.as_ref(), par)
2106 });
2107 assert!(max_diff(want.as_ref(), got.as_ref()) < 1e-8);
2108 }
2109}