Skip to main content

faer/utils/
team.rs

1//! The route of the parallel regions onto a threadteam team (feature `threadteam`).
2//!
3//! A `Par::Team(TeamExec)` is the exclusive authority over the reserved members of a team. The
4//! functions here consume that authority (or lend it for the call), so the type system, not a
5//! thread local, decides who may dispatch to the members:
6//!
7//! * flat sections ([`run`]): every participant runs a closure at once; the team is consumed;
8//! * splits and forks ([`fork`], [`for_each_child`]): each child is a `TeamExec` of its own,
9//!   handed to the callback that runs on its leader, concurrently with its siblings;
10//! * a team of one runs everything inline on the caller.
11//!
12//! Nothing here starts a Rayon job: the team members are threadteam members that spin while the
13//! reservation is held, and the section dispatch is a write to their slots.
14
15use core::cell::UnsafeCell;
16use rayon::iter::plumbing::{Producer, ProducerCallback};
17use rayon::iter::{IndexedParallelIterator, IntoParallelIterator};
18use threadteam::TeamExec;
19
20/// Runs `f` with the exclusive authority over a team of `width` participants.
21///
22/// This is how a caller owns the parallelism: the team is created for the duration of the closure,
23/// `Par::Team` carries the authority to the calls inside it, and the members are returned when the
24/// closure ends. A caller with a thread budget wraps its work once, rather than handing a number to
25/// every call:
26///
27/// ```
28/// use faer::{Accum, Mat};
29/// let a = Mat::<f64>::from_fn(8, 8, |i, j| (i + j) as f64);
30/// let mut dst = Mat::<f64>::zeros(8, 8);
31/// faer::team::with_team(2, |par| {
32///     // every faer call in here runs its regions on 2 participants: the calling thread and
33///     // 1 reserved worker
34///     faer::linalg::matmul::matmul(dst.as_mut(), Accum::Replace, a.as_ref(), a.as_ref(), 1.0, par);
35/// });
36/// ```
37///
38/// `width` is clamped to at least one; a width of one is [`crate::Par::Seq`] and reserves nothing.
39/// The team has `width` participants: the calling thread and `width - 1` reserved workers. The pool
40/// is the host's own ([`threadteam::Pool::builder`], `width` threads); a caller that already owns a
41/// pool or a team should borrow from it directly instead, because two pools do not share their
42/// members.
43///
44/// `with_team` is for a one-off call: every call builds a new pool, so it spawns its worker threads
45/// and joins them again when the closure ends. A caller that runs parallel work repeatedly should
46/// own the [`threadteam::Pool`] and [`threadteam::Team`] itself, build them once, and lend a `Par`
47/// from the team to each call:
48///
49/// ```
50/// use faer::{Accum, Mat, Par};
51/// let a = Mat::<f64>::from_fn(8, 8, |i, j| (i + j) as f64);
52/// let mut dst = Mat::<f64>::zeros(8, 8);
53///
54/// // once: 4 participants, the calling thread and 3 reserved workers
55/// let pool = threadteam::Pool::builder().threads(4).build().unwrap();
56/// let team = pool.into_team(4).ok().expect("an idle pool");
57///
58/// // per call: lend the team to faer for the duration of the closure
59/// for _ in 0..3 {
60///     let mut exec = team.exec(4);
61///     exec.team(|t| {
62///         faer::linalg::matmul::matmul(
63///             dst.as_mut(),
64///             Accum::Replace,
65///             a.as_ref(),
66///             a.as_ref(),
67///             1.0,
68///             Par::Team(t),
69///         );
70///     })
71///     .ok()
72///     .expect("a team token for an idle team");
73/// }
74/// ```
75///
76/// Panics if the team cannot be built or acquired: the closure's members wake asynchronously, so
77/// the acquisition waits as long as the team tests do (30 s) instead of threadteam's 2 ms default,
78/// which a loaded machine can miss.
79pub fn with_team<R>(width: usize, f: impl FnOnce(crate::Par<'_>) -> R) -> R {
80	// the dispatch counters are process wide: in tests, one team at a time keeps a test that reads
81	// them from seeing another test's regions
82	#[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/// Serializes the tests that hold a team (`utils::team::with_team` and the tests below), so the
104/// process-wide dispatch counters belong to one test at a time.
105///
106/// Reentrant on one thread: the outer acquisition takes the mutex, an inner one (a `with_team`
107/// inside a `with_team`, or a test that takes the gate itself) only counts its depth, so a nested
108/// scope cannot deadlock on a non-reentrant lock.
109#[cfg(test)]
110pub(crate) static TEAM_GATE: std::sync::Mutex<()> = std::sync::Mutex::new(());
111
112/// The gate's guard: holds the mutex only at depth one on this thread.
113#[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/// Takes the team turn for the current scope.
127#[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
140/// Optional counters of how the team regions were dispatched, for probes that ask "did the
141/// parallel path engage at this size". Off by default (one relaxed load per region).
142///
143/// The counters are process wide, so a test that reads them must be the only one dispatching a
144/// team while they are on: `TEAM_GATE` is what serializes that (see `with_team`).
145pub 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	/// flat sections by participant count (index 1 = ran inline on the caller)
153	pub(super) static RUN: [AtomicUsize; W] = [Z; W];
154	/// splits and forks by number of parts (index 1 = ran inline on the caller)
155	pub(super) static SPLIT: [AtomicUsize; W] = [Z; W];
156	/// bins of [`WIDTH`]: one per width up to 256 (the last bin collects anything wider)
157	const WW: usize = 257;
158	/// regions narrowed to a sub-team, by width (`thread::with_width` and the x86 kernel's fork)
159	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	/// Counters since the last `take`.
175	#[derive(Debug, Clone, Default)]
176	pub struct Stats {
177		/// `run` sections, index = participants
178		pub run: Vec<usize>,
179		/// splits and forks, index = parts
180		pub split: Vec<usize>,
181		/// regions narrowed to a sub-team, index = its width (up to 256)
182		pub width: Vec<usize>,
183		/// GEMM calls decided sequential by the work floor
184		pub gemm_below_floor: usize,
185		/// GEMM calls whose tile grid had a single tile
186		pub gemm_one_tile: usize,
187		/// GEMM calls cut into tiles and run on the team
188		pub gemm_tiled: usize,
189		/// products (plain, triangular or indexed) run by the x86 kernel on the team's own
190		/// members (`gemm_with` on a [`Fork`](private_gemm_x86::Fork) of the team)
191		pub gemm_team_kernel: usize,
192	}
193	/// Enables or disables counting.
194	pub fn enable(on: bool) {
195		ON.store(on, Relaxed);
196	}
197	/// Returns and resets the counters.
198	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
214/// Records a region narrowed to a sub-team of `width` (probes only).
215pub(crate) fn note_width(width: usize) {
216	stats::hist(&stats::WIDTH, width);
217}
218
219/// Records a split of the team into `parts` concurrent children (probes only).
220pub(crate) fn note_split(parts: usize) {
221	stats::hist(&stats::SPLIT, parts);
222}
223
224/// Runs `f(i, m)` on `m = min(n, team size)` participants at once; inline `f(0, 1)` when
225/// `m == 1`. The team is consumed; members the section does not use stay parked.
226pub(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
243/// A flat item loop on the team: the items of `iter` are dealt out in contiguous static blocks
244/// over `min(n_jobs, items, team size)` parts, each part processed by the leader of a sub-team.
245pub(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
257/// [`for_each`] whose closure also receives the sub-team of its block, lent for the call.
258///
259/// The items are not collected: the producer of `iter` is split on the leader into the blocks
260/// (`split_at`), and each block is iterated by the leader of its part, so the dispatch allocates
261/// nothing whatever the team's width (#111). An iterator adapter can still allocate in its own
262/// `split_at` (rayon's `map_with` clones its state).
263pub(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			// the producer may borrow from this callback's frame: every block is run and joined
289			// before it returns
290			deal(self.team, producer, self.len, self.jobs, self.f)
291		}
292	}
293	iter.with_producer(Deal { team, len, jobs, f: &f });
294}
295
296/// Blocks a single fork deals out of an inline array: up to this many, and only while the array
297/// stays small (`FLAT_BYTES` per slot, 16 KiB in all), since a producer can carry state of any
298/// size. Wider or heavier splits halve recursively instead, which costs a section per level
299/// and allocates nothing either.
300const FLAT_JOBS: usize = 64;
301const FLAT_BYTES: usize = 256;
302
303/// One producer piece, taken by one participant.
304struct Piece<P>(UnsafeCell<Option<P>>);
305// SAFETY: each piece is taken by exactly one participant (the one-taker invariant below).
306unsafe impl<P: Send> Sync for Piece<P> {}
307impl<P> Piece<P> {
308	/// # Safety
309	/// Called once per piece, by the participant that owns it.
310	unsafe fn take(&self) -> P {
311		unsafe { (*self.0.get()).take() }.expect("each piece is taken once")
312	}
313}
314
315/// Runs the `len` items of `producer` as `jobs` contiguous blocks (the boundaries of
316/// `par_split_indices`), block `c` on the leader of the `c`-th part of `team`.
317fn 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			// split relative to what is left: the earlier blocks are already taken off
335			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			// SAFETY: the fork hands index `c` to one child only
343			let piece = unsafe { pieces[c].take() };
344			for item in piece.into_iter() {
345				f(item, child.reborrow());
346			}
347		});
348		return;
349	}
350	// two halves, each with the participants of its blocks
351	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		// SAFETY: the split hands index `c` to one child only, and `c < 2`
360		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
385/// Granularity of tile edges: a multiple of the register tile of the `gemm` micro kernels
386/// (f64 on aarch64: 8 rows, 4 columns), so a tile does not end in a partial micro panel unless
387/// the matrix does.
388const ROW_GRAIN: usize = 8;
389const COL_GRAIN: usize = 4;
390
391/// The grid `(pm, pn)` with `pm * pn <= nt` that minimises the packed volume
392/// `k * (m / pm + n / pn)` per member, among grids that use as many members as the shape
393/// allows (every tile at least one grain wide).
394pub(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			// more members first, then the smaller packed volume
405			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
415/// `[start, end)` of part `idx` of `0..len` cut into `parts` blocks whose edges are multiples
416/// of `grain` (the last block takes the remainder).
417fn 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/// The instruction set and dtype of the `private_gemm_x86` kernel for `E`, if it has one here.
429#[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/// The `private_gemm_x86` kernel on a team: the kernel's parallel stages (shared packing of the
455/// right-hand side, then the dynamic job queue) run on the team's own members, through the
456/// kernel's `Fork` executor. Nothing starts a thread outside the team, and the triangular and
457/// indexed destinations of `spicy_matmul` and the triangular product need no tiling outside.
458#[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	/// Adapter of a team token (all of it, or a sub-team of the gate's width) to the kernel's
467	/// executor: `run(f)` is one team section, `f(j)` on member `j`, the calling thread being
468	/// member 0. The token is exclusive authority, so the adapter holds it behind a `RefCell`
469	/// and is not `Sync`, which `Fork` does not ask for.
470	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			// the kernel never re-enters `run`, so the borrow is never contended
483			self.team.borrow_mut().reborrow().run(|j, _| f(j));
484		}
485	}
486
487	/// The width at which `team` runs a product of serial time `estimate` through the kernel, or
488	/// `None` (run sequentially on the caller) when no width pays. The kernel forks twice per
489	/// call (packing, then compute): the widest width at which two sections pay.
490	pub(crate) fn kernel_width(team: &TeamExec<'_>, estimate: Duration) -> Option<usize> {
491		// the widest width that pays, the rule of every region (`cost::par_width_time`)
492		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	/// Calls `f` with an executor of `width` participants of `team` (a sub-team when `width` is
502	/// narrower than the team; the members it leaves out stay parked). The team is consumed.
503	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		// SAFETY: a split into the single child `[width]` runs the callback once, on the
515		// calling thread (child 0 is led by the caller), so the slot is never shared.
516		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			// SAFETY: see `Slot`.
525			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	/// The kernel route of [`gemm_team`](super::gemm_team): `None` when `E` has no kernel here.
534	pub(crate) fn available<E: 'static>() -> Option<(private_gemm_x86::InstrSet, private_gemm_x86::DType)> {
535		x86_kernel::<E>()
536	}
537}
538
539/// A plain product on a team token (`dst := alpha * dst + beta * lhs * rhs`, the contract of
540/// `gemm::gemm` as [`gemm_tiled`]): on x86_64 with AVX2 the `private_gemm_x86` kernel on the
541/// team's members ([`kernel::with_fork`], width from the gate [`kernel::kernel_width`], two
542/// sections); below the gate the same kernel on the calling thread. Other targets and dtypes
543/// take [`gemm_tiled`].
544///
545/// # Safety
546///
547/// As `gemm::gemm`.
548#[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		// `gemm::gemm` is `dst := alpha * dst + beta * lhs * rhs`, the kernel's
574		// `dst := (beta == Add ? dst : 0) + alpha * lhs * rhs`: `alpha` of the call is the
575		// identity whenever `read_dst` holds (the plain entry only passes 0 or 1)
576		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/// `dst := alpha * dst + beta * lhs * rhs` (the contract of `gemm::gemm`) with the output cut
624/// into a `pm x pn` grid of tiles, one sequential `gemm::gemm` per team member (the route of
625/// targets without the x86 kernel, see [`gemm_team`]). The depth `k`
626/// is never split, so no reduction and no partial buffer is needed. Each member's call blocks
627/// for the caches by itself (the `gemm` crate chooses `kc`, `mc` and `nc` from the cache sizes),
628/// so a tile is the unit of work of a member and the blocking inside it is unchanged.
629///
630/// Below the `gemm` crate's own threading floor, or on a single participant, the whole product
631/// is one sequential call.
632///
633/// # Safety
634///
635/// As `gemm::gemm`.
636#[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	// The generic route: one `gemm::gemm` per member with `Parallelism::None`. On x86_64 with
659	// AVX2 the team runs the `private_gemm_x86` kernel itself instead ([`gemm_team`]); this is the
660	// route of every other target.
661	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	// The floor is time, not a count: the serial time of the product (its MACs at the dtype's
685	// rate) against `PAYS` times the cost of one section, at the widest width that pays. A
686	// complex MAC is 4.5 real ones, so the floor of a complex product is that much lower in MACs.
687	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	// the section runs on `pm * pn` participants, which the grid can leave below `nt`: that width
699	// must pay as well (a row need not be monotone)
700	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		// `parts == pm * pn` unless the team is narrower, then the tiles are dealt round robin
717		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	/// Team tests take turns: one pool at a time, and the dispatch counters are not shared.
753	use super::team_turn;
754
755	/// Runs `f` with the authority over a team of `width` participants.
756	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	/// `with_team(width)` hands out `width` participants: the calling thread and `width - 1`
764	/// reserved workers. `into_team` clamps the width to the pool's, so a pool of `width - 1`
765	/// threads would give one participant fewer.
766	#[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	/// A factorization too small to pay for a section asks the gates of a team token as the
803	/// sequential path does: the same factors to the last bit, and no section opened. The block
804	/// apply decides once at its top (`householder.rs`, the floor of the whole apply) and runs its
805	/// products sequentially, instead of asking each product and each triangular step.
806	#[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	/// The rejection before the width search is against the cheapest width of the table, so it is right
837	/// for a table whose cost rises with the width: here 2 us at width 2 and 12 us at width 6, a
838	/// 13 us product (two sections) pays at width 2 only, and a 11 us one nowhere.
839	#[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	/// A measured row can fall with the width (aarch64-apple's team row does): a 7 us product
868	/// (two sections) pays at width 6 (1 us) but not at width 2 (3 us), so the search must not stop
869	/// at the first width that does not pay.
870	#[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	/// A team of `width` whose dispatch cost row is `anchors` (widths in increasing order).
896	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	/// The width of a region is the widest that pays, every width considered: on a row that
914	/// rises past 4 the region stops at 4, on a row that falls it takes a width past one that
915	/// does not pay, and a cap below the degree searches inside the cap.
916	#[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		// 1 us at 2 and 4, 100 us at 8: 6 us of work pays for one section at 4 (3 * 2 us) but not
921		// at 5 to 8 (interpolated 26.5 us and up)
922		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			// 5 us pays at 3 (interpolated 1.5 us) but not at 4
925			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		// a falling row: 9 us at 4, 1 us at 6 and 8; 3 us pays at 8 only, 9 us at 2 inside a cap of 5
930		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	/// `with_width` hands the region a sub-team of exactly that width, the whole token at its
939	/// degree or more, and `Seq` below two, and the parent keeps its width afterwards.
940	#[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			// only the narrowing to 3 is a sub-team; 8 and 9 are the token itself
966			assert_eq!(s.width[3], 1, "{s:?}");
967			assert_eq!(s.width.iter().sum::<usize>(), 1, "{s:?}");
968		});
969	}
970
971	/// Every converted family on a team of 8 whose row makes width 4 the widest that pays: the
972	/// regions narrow to 4 (counted) and the results equal the sequential ones.
973	#[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		// cheap up to 4, prohibitive from 5 on
979		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			// narrowed to 4 somewhere, and never wider than 4 in a split
1134			assert!(s.width[4] > 0, "{name}: no region narrowed to 4 ({s:?})");
1135			// and the family's own region (a split or a flat section, not only the GEMM kernel's
1136			// fork) ran on 4
1137			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	/// `Never` keeps the whole call sequential, the compact-factor upgrade after the steps included,
1146	/// even where the upgrade's own gate would pay (a 33 x 9 column pivoting QR at block 9 and a
1147	/// bidiagonalization, on a row that makes every width up to 4 cheap).
1148	#[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	/// The producer dealing of `for_each_child` (#111): every item exactly once, each on a child
1186	/// of an even share of the team, on the flat path, on the recursive one (more blocks than
1187	/// `FLAT_JOBS`, or a producer over `FLAT_BYTES`: `map_with` keeps its 512-byte state inline),
1188	/// with item, block and team counts that do not divide each other.
1189	#[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	/// A panic in one block reaches the caller after every block has finished, the items are
1225	/// dropped once each, and the team stays usable.
1226	#[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				// the team still works
1259				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	/// `par_pays_time` is `ParShape::pays` without the shape.
1270	#[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				// the tiles partition the output
1297				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				// transposed and strided operands
1330				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			// complex
1338			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	/// The floor is the serial time against the cost of a section, per dtype: a complex product
1356	/// reaches it at a quarter (4.5 times fewer MACs) of the real one. The old team floor counted
1357	/// MACs of every dtype against the real one and kept `c64` `64^3` (43 us serial) sequential.
1358	#[test]
1359	fn the_team_floor_is_per_dtype() {
1360		// the grid of `gemm_tiled` (no x86 kernel) or the kernel on the team's members
1361		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			// c64 64^3: 43 us serial, far above 3 sections' worth on every host table
1382			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			// f64 20^3 (8 ki MACs, 0.3 us) is below any floor
1386			let st = before(20, 20, 20, &mut par, false);
1387			assert_eq!(engaged(&st), 0, "{st:?}");
1388			// f64 192^3 (7 M MACs, 262 us) is above every floor
1389			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						// a job receives a share of the team no wider than the team
1422						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				// LU (partial pivoting) through the solver entry points with an explicit Par
1457				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				// QR
1478				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				// Cholesky of a well conditioned SPD matrix
1500				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				// compare the lower triangles
1526				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	/// Dispatch counters of `f`, which runs on a team of `width`.
1538	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		// the numeric phase forked the team into its four jobs; raw Rayon would record nothing
1570		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		// a swap stage with columns on both sides runs its two sides on a split of the team
1596		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		// it reads the process-wide counters
1632		let _turn = team_turn();
1633		threadteam::Exec::serial()
1634			.team(|team| {
1635				let par = Par::Team(team);
1636				assert_eq!(par.degree(), 1);
1637				// no gate pays on a single participant, however large the work
1638				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				// and the product equals the sequential one, with no team section at all
1642				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	/// The triangular products (the Cholesky trailing update) used to pass the team size to the
1659	/// `private_gemm_x86` kernel, outside the team. A team that pays now recurses on the team,
1660	/// its rectangular blocks reaching `gemm_tiled`.
1661	#[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		// the work ran as team sections, not as an outside job
1693		assert!(st.gemm_tiled > 0 || st.gemm_team_kernel > 0 || st.split.iter().skip(2).any(|&c| c > 0), "{st:?}");
1694	}
1695
1696	/// `spicy_matmul` (LDLT, supernodal Cholesky) on a team token: one thread per participant
1697	/// through the x86 kernel, the same result as sequential.
1698	#[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	/// `gemm_tiled` (cut into tiles, through the x86 kernel where there is one) against the `gemm` crate, for the four dtypes and
1725	/// both accumulation modes.
1726	#[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	/// Records the thread of every participant of the fork it wraps.
1791	#[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	/// The team route starts no thread outside the team: every participant the kernel forks on
1814	/// is the caller or a member of the team token (whole team and a narrower sub-team), the
1815	/// kernel forks twice for a product that packs (once for one that does not), and the result
1816	/// is the sequential one.
1817	#[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			// the threads of the members
1833			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	/// Every route that reaches the kernel on a team agrees with sequential, and the counters
1872	/// show the kernel ran on the team (not `gemm_tiled`, not an outside runtime).
1873	#[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	/// `spicy_matmul` and the triangular product on a team (kernel, lower and upper, indexed
1915	/// destination) and the factorizations built on them: Cholesky (both), LDLT, QR and LU on a
1916	/// team equal their sequential results, and the kernel ran on the team.
1917	#[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		// spicy_matmul: both triangles, plain
1940		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		// spicy_matmul: gathered rows and columns into a larger destination (a scatter)
1952		{
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		// the triangular product into a triangle
1969		{
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		// factorizations
1981		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		// the panel products of a 640 QR are below the floor, so no counter claim here: the
2034		// kernel's own coverage is the plain, triangular, indexed, Cholesky, LDLT and LU rows
2035		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	/// QR at a size whose block-reflector products are above the gate: they run on the kernel.
2050	#[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	/// The high level methods take the parallelism as an argument (there is no process wide
2072	/// setting): `singular_values(Par::Team(..))` runs on the team and matches the sequential one.
2073	#[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	/// A solver struct built with a team and used with a team: the work happens where `par` is given.
2088	#[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}