Skip to main content

faer/linalg/lu/partial_pivoting/
factor.rs

1use crate::internal_prelude::*;
2use crate::perm::swap_rows_idx;
3use crate::{assert, debug_assert};
4#[inline]
5pub(crate) fn swap_elems<T: ComplexField>(
6	col: ColMut<'_, T>,
7	i: usize,
8	j: usize,
9) {
10	debug_assert!(all(i < col.nrows(), j < col.nrows()));
11	let rs = col.row_stride();
12	let col = col.as_ptr_mut();
13	unsafe {
14		let a = col.offset(i as isize * rs);
15		let b = col.offset(j as isize * rs);
16		core::ptr::swap(a, b);
17	}
18}
19fn lu_in_place_unblocked<I: Index, T: ComplexField>(
20	matrix: MatMut<'_, T>,
21	start: usize,
22	end: usize,
23	trans: &mut [I],
24) -> usize {
25	let mut matrix = matrix;
26	let m = matrix.nrows();
27	if start == end {
28		return 0;
29	}
30	let mut n_trans = 0;
31	for j in start..end {
32		let col = j;
33		let row = j - start;
34		let t = &mut trans[row];
35		let mut imax = row;
36		let mut max = zero();
37		for i in imax..m {
38			let abs = matrix[(i, col)].abs1();
39			if abs > max {
40				max = abs;
41				imax = i;
42			}
43		}
44		*t = I::truncate(imax - row);
45		if imax != row {
46			swap_rows_idx(matrix.rb_mut(), row, imax);
47			n_trans += 1;
48		}
49		let mut matrix = matrix.rb_mut().get_mut(.., start..end);
50		// An exactly zero pivot column is a singular factor, not a division: OpenBLAS's
51		// `getf2_k` leaves the (all zero) column alone instead of scaling it by infinity.
52		let pivot = matrix[(row, row)].copy();
53		if pivot != zero() {
54			if pivot.abs() >= min_positive() {
55				let ref inv = pivot.recip();
56				for i in row + 1..m {
57					matrix[(i, row)] *= inv;
58				}
59			} else {
60				// the reciprocal of a subnormal pivot overflows, and the complex quotient
61				// forms its squared denominator, so rescale both operands instead: a power
62				// of two keeps the scaling exact and the squares in range (`dgetf2`
63				// divides elementwise below `SFMIN`)
64				let ref shift = max_positive::<T::Real>();
65				let ref pivot = pivot.mul_real(shift);
66				for i in row + 1..m {
67					matrix[(i, row)] =
68						&matrix[(i, row)].mul_real(shift) / pivot;
69				}
70			}
71		}
72		let (_, A01, A10, A11) = matrix.rb_mut().split_at_mut(row + 1, row + 1);
73		let A01 = A01.row(row);
74		let A10 = A10.col(row);
75		linalg::matmul::matmul(
76			A11,
77			Accum::Add,
78			A10.as_mat(),
79			A01.as_mat(),
80			-one::<T>(),
81			Par::Seq,
82		);
83	}
84	n_trans
85}
86/// Sections of the trailing update of one recursion level: the triangular solve and the
87/// product, each a join; the second runs on caches the first filled or evicted, so the two
88/// are counted as the cold-region rule of the thresholds design (provisional).
89const TRAILING_UPDATE_SECTIONS: u32 = 2;
90/// Sections of the swap stage of one level when both the columns left of the panel and the
91/// columns right of it are swapped: a split of the two blocks over the parallelism, then a
92/// split of the columns of each; memory bound, so a section costs more than its share of the
93/// work (provisional, from the thresholds design).
94const SWAP_STAGE_SECTIONS: u32 = 4;
95/// Sections of the swap stage when one side holds every column (the first panel of a wide
96/// matrix, the last of a tall one): one split of the columns over the whole budget.
97const SWAP_ONE_SIDE_SECTIONS: u32 = 1;
98use crate::utils::cost::{gemm_ns_per_mac, par_width_time, rate, serial_time};
99use crate::utils::thread::with_width;
100
101pub use crate::utils::cost::ParThreshold;
102
103/// The width the trailing update (`b x cols` solve, `rows x cols x b` product) of a level runs
104/// at (1: sequential). [`ParThreshold::Derived`] is the widest width of `par` that pays for the
105/// serial time; [`Fixed`](ParThreshold::Fixed) is an explicit floor in `f64` elements of the
106/// updated block, counted in flops (a complex element carries four times the arithmetic), and
107/// takes the whole degree above it.
108fn trailing_update_width<T: ComplexField>(
109	par: &Par,
110	par_threshold: ParThreshold,
111	rows: usize,
112	cols: usize,
113	b: usize,
114) -> usize {
115	let floor = match par_threshold {
116		ParThreshold::Never => return 1,
117		ParThreshold::Fixed(n) => n,
118		ParThreshold::Derived => {
119			let macs = cols.saturating_mul(
120				b.saturating_mul(b) / 2 + rows.saturating_mul(b),
121			);
122			return par_width_time(
123				par,
124				serial_time(macs, gemm_ns_per_mac::<T>()),
125				TRAILING_UPDATE_SECTIONS,
126			);
127		},
128	};
129	let work = rows
130		.saturating_mul(cols)
131		.saturating_mul(if T::IS_REAL { 2 } else { 8 });
132	if work > floor.saturating_mul(2) { par.degree() } else { 1 }
133}
134
135/// The width the swap stage (`n` row swaps over `cols` columns of an `m` row matrix) runs at;
136/// `par_threshold` as in [`trailing_update_width`], where an explicit value counts `m * cols`
137/// elements.
138fn swap_stage_width<T: ComplexField>(
139	par: &Par,
140	par_threshold: ParThreshold,
141	m: usize,
142	n: usize,
143	cols: usize,
144	one_side: bool,
145) -> usize {
146	match par_threshold {
147		ParThreshold::Never => 1,
148		ParThreshold::Fixed(floor) => {
149			if m.saturating_mul(cols) > floor { par.degree() } else { 1 }
150		},
151		ParThreshold::Derived => par_width_time(
152			par,
153			serial_time(n.saturating_mul(cols), rate::lu_swap().of::<T>()),
154			if one_side { SWAP_ONE_SIDE_SECTIONS } else { SWAP_STAGE_SECTIONS },
155		),
156	}
157}
158pub(crate) fn lu_in_place_recursion<I: Index, T: ComplexField>(
159	A: MatMut<'_, T>,
160	start: usize,
161	end: usize,
162	trans: &mut [I],
163	mut par: Par,
164	params: Spec<PartialPivLuParams, T>,
165) -> usize {
166	let params = params.config;
167	let mut A = A;
168	let m = A.nrows();
169	let ncols = A.ncols();
170	let n = end - start;
171	if n <= params.recursion_threshold {
172		return lu_in_place_unblocked(A, start, end, trans);
173	}
174	let half = n / 2;
175	let pow = Ord::min(16, half.next_power_of_two());
176	let block_size = half.next_multiple_of(pow);
177	let mut n_trans = 0;
178	assert!(n <= m);
179	n_trans += lu_in_place_recursion(
180		A.rb_mut().get_mut(.., start..end),
181		0,
182		block_size,
183		&mut trans[..block_size],
184		par.reborrow(),
185		params.into(),
186	);
187	{
188		let mut A = A.rb_mut().get_mut(.., start..end);
189		let (A00, mut A01, A10, mut A11) =
190			A.rb_mut().split_at_mut(block_size, block_size);
191		let A00 = A00.rb();
192		let A10 = A10.rb();
193		// The panel solve and the trailing product below are this level's whole
194		// parallel work, and the block size halves every level of the recursion, so
195		// past a few levels they are a fork and a join around a handful of thousands
196		// of flops. The region is decided by its serial time against the cost of its
197		// sections: the solve (`b * b / 2` MACs per column of `A01`) and the product
198		// (`b` MACs per element of `A11`) at the dtype's GEMM rate, two sections
199		// (one join for the solve, one for the product), the second of them on
200		// cold caches. Below it take `Par::Seq`; the recursion keeps the caller's
201		// `par`, so every level decides for its own trailing update.
202		let (rows1, cols1, b) = (A11.nrows(), A11.ncols(), A10.ncols());
203		let width = trailing_update_width::<T>(&par, params.par_threshold, rows1, cols1, b);
204		with_width(par.reborrow(), width, |mut par_here| {
205	#[cfg(feature = "rayon")]
206			{
207				linalg::triangular_solve::solve_unit_lower_triangular_in_place(
208					A00.rb(),
209					A01.rb_mut(),
210					par_here.reborrow(),
211				);
212			}
213			linalg::matmul::matmul(
214				A11.rb_mut(),
215				Accum::Add,
216				A10.rb(),
217				A01.rb(),
218				-one::<T>(),
219				par_here,
220			);
221		});
222		n_trans += lu_in_place_recursion(
223			A.rb_mut().get_mut(block_size..m, ..),
224			block_size,
225			n,
226			&mut trans[block_size..n],
227			par.reborrow(),
228			params.into(),
229		);
230	}
231	let swap = |mat: MatMut<'_, T>| {
232		let mut mat = mat;
233		for j in 0..mat.ncols() {
234			let mut col = mat.rb_mut().col_mut(j);
235			if col.row_stride() == 1 {
236				for (j, &t) in trans[..n].iter().enumerate() {
237					swap_elems(col.rb_mut(), j, t.zx() + j);
238				}
239			} else {
240				for (j, &t) in trans[..n].iter().enumerate() {
241					swap_elems(col.rb_mut(), j, t.zx() + j);
242				}
243			}
244		}
245	};
246	let (A_left, A_right) = A.rb_mut().split_at_col_mut(start);
247	let A_right = A_right.get_mut(.., end - start..ncols - start);
248	let width = swap_stage_width::<T>(
249		&par,
250		params.par_threshold,
251		m,
252		n,
253		ncols - n,
254		start == 0 || end == ncols,
255	);
256	with_width(par.reborrow(), width, |par| {
257		match par {
258			Par::Seq => {
259				swap(A_left);
260				swap(A_right);
261			},
262		#[cfg(feature = "rayon")]
263			_ => {
264			let nthreads = core::num::NonZeroUsize::new(par.degree()).unwrap();
265				let nthreads = nthreads.get();
266				let len = (A_left.ncols() + A_right.ncols()) as f64;
267				let left_threads = Ord::min(
268					(nthreads as f64 * (A_left.ncols() as f64 / len)) as usize,
269					nthreads,
270				);
271				let (left_cols, right_cols) = (A_left.ncols(), A_right.ncols());
272				if left_cols == 0 || right_cols == 0 {
273					// one side holds every column: it takes the whole budget
274					let (A, _) = if left_cols == 0 { (A_right, A_left) } else { (A_left, A_right) };
275					crate::utils::thread::for_each(
276						par,
277						nthreads,
278						A.par_col_partition_mut(nthreads),
279						|A| swap(A),
280					);
281				} else {
282					// the two sides run on the two halves of the split, each on the
283					// parallelism it is handed
284					crate::utils::thread::join_sized(
285						par,
286						left_threads,
287						|par| {
288							let n = par.degree();
289							crate::utils::thread::for_each(
290								par,
291								n,
292								A_left.par_col_partition_mut(n),
293								|A| swap(A),
294							)
295						},
296						|par| {
297							let n = par.degree();
298							crate::utils::thread::for_each(
299								par,
300								n,
301								A_right.par_col_partition_mut(n),
302								|A| swap(A),
303							)
304						},
305					);
306				}
307			},
308		}
309	});
310	n_trans
311}
312/// $LU$ factorization tuning parameters
313#[derive(Copy, Clone, Debug)]
314pub struct PartialPivLuParams {
315	/// threshold at which the implementation should stop recursing
316	pub recursion_threshold: usize,
317	/// blocking variant step size
318	pub block_size: usize,
319	/// floor of the parallel regions; [`ParThreshold::Derived`] (the default) derives them from
320	/// the serial time of each region and the dispatch cost of the parallelism
321	/// ([`ParShape::dispatch_cost`]), per region: the trailing update of a recursion level
322	/// (2 sections) and the swap stage (4 sections). [`ParThreshold::Fixed`] restores a fixed
323	/// floor, [`ParThreshold::Never`] disables parallelism. The trailing update and the swap
324	/// stage keep separate rules.
325	pub par_threshold: ParThreshold,
326	#[doc(hidden)]
327	pub non_exhaustive: NonExhaustive,
328}
329/// information about the resulting $LU$ factorization
330#[derive(Copy, Clone, Debug)]
331pub struct PartialPivLuInfo {
332	/// number of transpositions that were performed, can be used to compute
333	/// the determinant of $P$
334	pub transposition_count: usize,
335}
336/// error in the $LU$ factorization
337#[derive(Copy, Clone, Debug)]
338pub enum LdltError {
339	ZeroPivot { index: usize },
340}
341impl<T: ComplexField> Auto<T> for PartialPivLuParams {
342	#[inline]
343	fn auto() -> Self {
344		Self {
345			recursion_threshold: 16,
346			block_size: 64,
347			par_threshold: ParThreshold::Derived,
348			non_exhaustive: NonExhaustive(()),
349		}
350	}
351}
352#[inline]
353pub fn lu_in_place_scratch<I: Index, T: ComplexField>(
354	nrows: usize,
355	ncols: usize,
356	par: ParShape,
357	params: Spec<PartialPivLuParams, T>,
358) -> StackReq {
359	_ = par;
360	_ = params;
361	StackReq::new::<I>(Ord::min(nrows, ncols))
362}
363pub fn lu_in_place<'out, I: Index, T: ComplexField>(
364	A: MatMut<'_, T>,
365	perm: &'out mut [I],
366	perm_inv: &'out mut [I],
367	mut par: Par,
368	stack: &mut MemStack,
369	params: Spec<PartialPivLuParams, T>,
370) -> (PartialPivLuInfo, PermRef<'out, I>) {
371	let _ = &params;
372	let truncate = I::truncate;
373	#[cfg(feature = "perf-warn")]
374	if (A.col_stride().unsigned_abs() == 1
375		|| A.row_stride().unsigned_abs() != 1)
376		&& crate::__perf_warn!(LU_WARN)
377	{
378		log::warn!(
379			target : "faer_perf",
380			"LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides."
381		);
382	}
383	let mut matrix = A;
384	let mut stack = stack;
385	let m = matrix.nrows();
386	let n = matrix.ncols();
387	// Row-major input: the unblocked core walks each pivot column twice (the
388	// search, then the scaling) and the swap stage swaps elements inside a
389	// column, which is a cache line per element when the columns are not
390	// contiguous -- `size * m` touches that way against `m * n` elements the copy
391	// moves contiguously twice. Factor a column-major copy and write the factor
392	// back, which is what LAPACK's row-major interface pays internally; the
393	// arithmetic is the copy's, so the factor and the permutation are the
394	// column-major ones. The guard is the panel's own cost model, the same one
395	// `qr_in_place` states: a wide matrix has `size == m` over `n` columns, where
396	// the copy would cost more than it saved.
397	if matrix.row_stride().unsigned_abs() != 1
398		&& matrix.nrows() >= matrix.ncols()
399		&& matrix.ncols() > 1
400		&& linalg::row_major_copy_pays(matrix.nrows(), matrix.ncols())
401	{
402		let mut work = linalg::column_major_copy(matrix.rb(), par.reborrow());
403		let mut work = MatMut::from_column_major_slice_mut(&mut work, m, n);
404		let ret = lu_in_place(
405			work.rb_mut(),
406			perm,
407			perm_inv,
408			par.reborrow(),
409			stack,
410			params,
411		);
412		linalg::copy_across_layouts(matrix, work.rb(), par);
413		return ret;
414	}
415	let size = Ord::min(n, m);
416	for i in 0..m {
417		let p = &mut perm[i];
418		*p = truncate(i);
419	}
420	let (mut transpositions, _) =
421		stack.rb_mut().make_with(size, |_| truncate(0));
422	let transpositions = transpositions.as_mut();
423	let n_transpositions = lu_in_place_recursion(
424		matrix.rb_mut(),
425		0,
426		size,
427		transpositions.as_mut(),
428		par.reborrow(),
429		params,
430	);
431	for idx in 0..size {
432		let t = transpositions[idx];
433		perm.as_mut().swap(idx, idx + t.zx());
434	}
435	if m < n {
436		let (left, right) = matrix.split_at_col_mut(size);
437		linalg::triangular_solve::solve_unit_lower_triangular_in_place(
438			left.rb(),
439			right,
440			par,
441		);
442	}
443	for i in 0..m {
444		perm_inv[perm[i].zx()] = truncate(i);
445	}
446	(
447		PartialPivLuInfo {
448			transposition_count: n_transpositions,
449		},
450		unsafe { PermRef::new_unchecked(perm, perm_inv, m) },
451	)
452}
453#[cfg(test)]
454mod tests {
455	use super::*;
456	use crate::stats::prelude::*;
457	use crate::utils::approx::*;
458	use crate::{Mat, assert, c32, c64};
459	use crate::utils::cost::{pays_at, rate, serial_time};
460	use dyn_stack::MemBuffer;
461	#[cfg(feature = "rayon")]
462	#[test]
463	fn region_rules_follow_the_serial_time() {
464		use core::num::NonZeroUsize;
465		let seq = Par::Seq;
466		// sequential never goes wider than one, whatever the size
467		assert_eq!(trailing_update_width::<f64>(&seq, ParThreshold::Derived, 4096, 4096, 256), 1);
468		assert_eq!(swap_stage_width::<f64>(&seq, ParThreshold::Derived, 4096, 256, 4096, false), 1);
469		crate::team::with_team(6, |par| {
470			// tiny regions stay serial, huge ones take the whole team
471			assert_eq!(trailing_update_width::<f64>(&par, ParThreshold::Derived, 16, 16, 8), 1);
472			assert_eq!(trailing_update_width::<f64>(&par, ParThreshold::Derived, 2048, 2048, 128), 6);
473			assert_eq!(swap_stage_width::<f64>(&par, ParThreshold::Derived, 64, 16, 48, false), 1);
474			assert_eq!(swap_stage_width::<f64>(&par, ParThreshold::Derived, 4096, 256, 4096, false), 6);
475			// a complex trailing update reaches the floor at fewer elements than a real one
476			let (r, c) = (160, 160);
477			let real = trailing_update_width::<f64>(&par, ParThreshold::Derived, r, c, 32);
478			let cplx = trailing_update_width::<c64>(&par, ParThreshold::Derived, r, c, 32);
479			assert!(cplx >= real);
480			// an explicit floor restores the fixed rule (the whole team) and `usize::MAX` disables
481			assert_eq!(trailing_update_width::<f64>(&par, ParThreshold::Fixed(1), 64, 64, 8), 6);
482			assert_eq!(trailing_update_width::<f64>(&par, ParThreshold::Never, 4096, 4096, 256), 1);
483			assert_eq!(swap_stage_width::<f64>(&par, ParThreshold::Never, 4096, 256, 4096, false), 1);
484		});
485		// the swap stage of a 64 x 1024 matrix (64 swaps over 960 columns, every column on one
486		// side: one section) takes the widest width at which the rule holds on the tables of the
487		// host class, for both dtypes; the same region counted as the two sided split (four
488		// sections) is what kept it serial. The verdict depends on the class, so the test states
489		// the rule.
490		for w in [4, 8] {
491			crate::team::with_team(w, |par| {
492				let shape = par.shape();
493				let rule = |ns: f64| {
494					(2..=w)
495						.rev()
496						.find(|&k| pays_at(serial_time(64 * 960, ns), SWAP_ONE_SIDE_SECTIONS, shape.dispatch_cost(NonZeroUsize::new(k).unwrap())))
497						.unwrap_or(1)
498				};
499				assert_eq!(swap_stage_width::<f64>(&par, ParThreshold::Derived, 64, 64, 960, true), rule(rate::lu_swap().of::<f64>()), "f64 {w}");
500				assert_eq!(swap_stage_width::<c64>(&par, ParThreshold::Derived, 64, 64, 960, true), rule(rate::lu_swap().of::<c64>()), "c64 {w}");
501			});
502		}
503	}
504
505	/// The wide shape of the merge gate (`64 x 1024`): every column of the swap stage is on one
506	/// side (one section), and the right block goes through the flat split of the triangular
507	/// solve, at the widths the caller reserves.
508	#[cfg(feature = "rayon")]
509	#[test]
510	fn wide_matrix_parallel_agrees_with_serial() {
511		fn check<T: ComplexField<Real = f64>>(m: usize, n: usize) {
512			let a = Mat::<T>::from_fn(m, n, |i, j| {
513				let h = (i * 2654435761usize).wrapping_add(j * 40503) % 1009;
514				from_f64::<T>(h as f64 / 504.0 - 1.0)
515			});
516			let run = |mut par: Par| {
517				let mut lu = a.clone();
518				let mut perm = vec![0usize; m];
519				let mut perm_inv = vec![0usize; m];
520				let mut mem = MemBuffer::new(lu_in_place_scratch::<usize, T>(m, n, par.shape(), default()));
521				lu_in_place(lu.as_mut(), &mut perm, &mut perm_inv, par.reborrow(), MemStack::new(&mut mem), default());
522				(lu, perm)
523			};
524			let (want, perm_want) = run(Par::Seq);
525			for threads in [4, 8] {
526				{
527					let (got, perm_got) = crate::team::with_team(threads, run);
528					assert_eq!(perm_want, perm_got, "{m}x{n} {threads} threads");
529					let mut d: f64 = 0.0;
530					for j in 0..n {
531						for i in 0..m {
532							d = d.max((want[(i, j)].clone() - got[(i, j)].clone()).abs());
533						}
534					}
535					assert!(d < 1e-9, "{m}x{n} {threads} threads: {d}");
536				}
537			}
538		}
539		check::<f64>(64, 1024);
540		check::<c64>(64, 1024);
541		check::<f64>(48, 700);
542	}
543
544	#[test]
545	fn derived_gates_keep_the_factorization() {
546		let rng = &mut StdRng::seed_from_u64(7);
547		for n in [96, 200, 384] {
548			let A = CwiseMatDistribution {
549				nrows: n,
550				ncols: n,
551				dist: StandardNormal,
552			}
553			.rand::<Mat<f64>>(rng);
554			let mut want = A.clone();
555			let mut got = A.clone();
556			let mut perm_a = vec![0usize; n];
557			let mut perm_ai = vec![0usize; n];
558			let mut perm_b = vec![0usize; n];
559			let mut perm_bi = vec![0usize; n];
560			let mut mem = MemBuffer::new(lu_in_place_scratch::<usize, f64>(n, n, Par::Seq.shape(), default()));
561			lu_in_place(want.as_mut(), &mut perm_a, &mut perm_ai, Par::Seq, MemStack::new(&mut mem), default());
562			crate::team::with_team(4, |par| {
563				lu_in_place(got.as_mut(), &mut perm_b, &mut perm_bi, par, MemStack::new(&mut mem), default())
564			});
565			assert_eq!(perm_a, perm_b, "n = {n}");
566			let mut d: f64 = 0.0;
567			for j in 0..n {
568				for i in 0..n {
569					d = d.max((want[(i, j)] - got[(i, j)]).abs());
570				}
571			}
572			assert!(d < 1e-9, "n = {n}: {d}");
573		}
574	}
575
576	#[test]
577	fn test_plu() {
578		let rng = &mut StdRng::seed_from_u64(0);
579		let approx_eq = CwiseMat(ApproxEq {
580			abs_tol: 1e-13,
581			rel_tol: 1e-13,
582		});
583		for n in [1, 2, 3, 128, 255, 256, 257] {
584			let A = CwiseMatDistribution {
585				nrows: n,
586				ncols: n,
587				dist: StandardNormal,
588			}
589			.rand::<Mat<f64>>(rng);
590			let A = A.as_ref();
591			let mut LU = A.cloned();
592			let perm = &mut *vec![0usize; n];
593			let perm_inv = &mut *vec![0usize; n];
594			let params = PartialPivLuParams {
595				recursion_threshold: 2,
596				block_size: 2,
597				..auto!(f64)
598			};
599			let p = lu_in_place(
600				LU.as_mut(),
601				perm,
602				perm_inv,
603				Par::Seq,
604				MemStack::new(&mut MemBuffer::new(lu_in_place_scratch::<
605					usize,
606					f64,
607				>(
608					n,
609					n,
610					ParShape::Seq,
611					params.into(),
612				))),
613				params.into(),
614			)
615			.1;
616			let mut L = LU.as_ref().cloned();
617			let mut U = LU.as_ref().cloned();
618			for j in 0..n {
619				for i in 0..j {
620					L[(i, j)] = 0.0;
621				}
622				L[(j, j)] = 1.0;
623			}
624			for j in 0..n {
625				for i in j + 1..n {
626					U[(i, j)] = 0.0;
627				}
628			}
629			let L = L.as_ref();
630			let U = U.as_ref();
631			assert!(p.inverse() * L * U ~ A);
632		}
633		for m in [8, 128, 255, 256, 257] {
634			let n = 8;
635			let A = CwiseMatDistribution {
636				nrows: m,
637				ncols: n,
638				dist: StandardNormal,
639			}
640			.rand::<Mat<f64>>(rng);
641			let A = A.as_ref();
642			let mut LU = A.cloned();
643			let perm = &mut *vec![0usize; m];
644			let perm_inv = &mut *vec![0usize; m];
645			let p = lu_in_place(
646				LU.as_mut(),
647				perm,
648				perm_inv,
649				Par::Seq,
650				MemStack::new(&mut MemBuffer::new(lu_in_place_scratch::<
651					usize,
652					f64,
653				>(
654					n, n, ParShape::Seq, default()
655				))),
656				default(),
657			)
658			.1;
659			let mut L = LU.as_ref().cloned();
660			let mut U = LU.as_ref().cloned();
661			for j in 0..n {
662				for i in 0..j {
663					L[(i, j)] = 0.0;
664				}
665				L[(j, j)] = 1.0;
666			}
667			for j in 0..n {
668				for i in j + 1..m {
669					U[(i, j)] = 0.0;
670				}
671			}
672			let L = L.as_ref();
673			let U = U.as_ref();
674			let U = U.subrows(0, n);
675			assert!(p.inverse() * L * U ~ A);
676		}
677	}
678	fn factor_lu(A: &Mat<f64>, par: Par) -> (Mat<f64>, Vec<usize>, usize) {
679		let n = A.nrows();
680		let mut LU = A.cloned();
681		let perm = &mut *vec![0usize; n];
682		let perm_inv = &mut *vec![0usize; n];
683		let params: Spec<PartialPivLuParams, f64> = default();
684		let stack =
685			&mut MemBuffer::new(lu_in_place_scratch::<usize, f64>(n, n, par.shape(), params));
686		let stack = MemStack::new(stack);
687		let (info, _) = lu_in_place(LU.as_mut(), perm, perm_inv, par, stack, params);
688		(LU, perm.to_vec(), info.transposition_count)
689	}
690	fn factor_lu_c64(A: &Mat<c64>, par: Par) -> (Mat<c64>, Vec<usize>, usize) {
691		let n = A.nrows();
692		let mut LU = A.cloned();
693		let perm = &mut *vec![0usize; n];
694		let perm_inv = &mut *vec![0usize; n];
695		let params: Spec<PartialPivLuParams, c64> = default();
696		let stack =
697			&mut MemBuffer::new(lu_in_place_scratch::<usize, c64>(n, n, par.shape(), params));
698		let stack = MemStack::new(stack);
699		let (info, _) = lu_in_place(LU.as_mut(), perm, perm_inv, par, stack, params);
700		(LU, perm.to_vec(), info.transposition_count)
701	}
702	/// The two products at a recursion level take `Par::Seq` while the trailing update's work is
703	/// below the floor, instead of the caller's hint. That is a choice of how to schedule the
704	/// same arithmetic, so both sides of the floor have to return the same factorization: `96`,
705	/// `128` and `192` are below `128 * 128` `f64` elements at the top level, `256` and `512`
706	/// above it, and every level in between halves down through it. A `c64` element is four real
707	/// MACs, so the same floor is reached at a quarter of the area -- `128` is below it and `256`
708	/// above it for `c64` too, which is the case the element-only comparison got wrong.
709	#[test]
710	fn threaded_lu_matches_the_sequential_one() {
711		let rng = &mut StdRng::seed_from_u64(0);
712		let approx_eq = CwiseMat(ApproxEq {
713			abs_tol: 1e-12,
714			rel_tol: 1e-12,
715		});
716		for n in [96, 128, 192, 256, 512] {
717			let A = CwiseMatDistribution {
718				nrows: n,
719				ncols: n,
720				dist: StandardNormal,
721			}
722			.rand::<Mat<f64>>(rng);
723			let (seq, perm_seq, trans_seq) = factor_lu(&A, Par::Seq);
724			let (par, perm_par, trans_par) =
725				crate::team::with_team(4, |par| factor_lu(&A, par));
726			assert!(seq.as_ref() ~ par.as_ref());
727			assert!(perm_seq == perm_par);
728			assert!(trans_seq == trans_par);
729		}
730		for n in [96, 128, 192, 256, 512] {
731			let real = CwiseMatDistribution {
732				nrows: n,
733				ncols: n,
734				dist: StandardNormal,
735			}
736			.rand::<Mat<f64>>(rng);
737			let imag = CwiseMatDistribution {
738				nrows: n,
739				ncols: n,
740				dist: StandardNormal,
741			}
742			.rand::<Mat<f64>>(rng);
743			let A = Mat::<c64>::from_fn(n, n, |i, j| c64::new(real[(i, j)], imag[(i, j)]));
744			let (seq, perm_seq, trans_seq) = factor_lu_c64(&A, Par::Seq);
745			let (par, perm_par, trans_par) =
746				crate::team::with_team(4, |par| factor_lu_c64(&A, par));
747			for j in 0..n {
748				for i in 0..n {
749					let diff = (seq[(i, j)] - par[(i, j)]).abs();
750					assert!(diff <= 1e-10, "({i}, {j}) differs by {diff:e}");
751				}
752			}
753			assert!(perm_seq == perm_par);
754			assert!(trans_seq == trans_par);
755		}
756	}
757	/// An exactly zero pivot column is a singular matrix, not a failed factorization: the
758	/// factors stay finite and still reconstruct the input, with the singularity left in `U`.
759	/// A subnormal pivot must not be scaled by its infinite reciprocal either. OpenBLAS's
760	/// `getf2_k` skips the column when the pivot is exactly zero, and reference `dgetf2`
761	/// divides elementwise below `SFMIN`.
762	#[test]
763	fn zero_pivot_column_reconstructs() {
764		fn zero_pivot<T: ComplexField>(n: usize) -> Mat<T> {
765			Mat::<T>::from_fn(n, n, |i, j| {
766				if j > 0 {
767					from_f64::<T>((i + 2 * j) as f64)
768				} else {
769					zero()
770				}
771			})
772		}
773		// the pivot is the largest entry of a column, so a subnormal pivot needs a
774		// subnormal fixture: `tiny` is the smallest useful one for each dtype
775		fn subnormal<T: ComplexField>(tiny: f64) -> Mat<T> {
776			let tiny = from_f64::<T>(tiny);
777			Mat::<T>::from_fn(2, 2, |i, j| match (i, j) {
778				(0, 0) => tiny.copy(),
779				(0, 1) => one(),
780				(1, 0) => tiny.mul_real(from_f64::<T::Real>(0.5)),
781				(1, 1) => from_f64::<T>(2.0),
782				_ => zero(),
783			})
784		}
785		for n in [1usize, 2, 3, 128] {
786			check_factor::<f32>(zero_pivot::<f32>(n).as_ref(), None);
787			check_factor::<c32>(zero_pivot::<c32>(n).as_ref(), None);
788			check_factor::<f64>(zero_pivot::<f64>(n).as_ref(), None);
789			check_factor::<c64>(zero_pivot::<c64>(n).as_ref(), None);
790		}
791		// a power of two so that the subnormal and its half are both exact and the
792		// expected multiplier is exactly `0.5`
793		check_factor::<f32>(
794			subnormal::<f32>(2f64.powi(-147)).as_ref(),
795			Some(0.5f32),
796		);
797		check_factor::<c32>(
798			subnormal::<c32>(2f64.powi(-147)).as_ref(),
799			Some(c32::new(0.5, 0.0)),
800		);
801		check_factor::<f64>(
802			subnormal::<f64>(2f64.powi(-1072)).as_ref(),
803			Some(0.5f64),
804		);
805		check_factor::<c64>(
806			subnormal::<c64>(2f64.powi(-1072)).as_ref(),
807			Some(c64::new(0.5, 0.0)),
808		);
809	}
810	fn check_factor<T: ComplexField>(a: MatRef<'_, T>, l10: Option<T>) {
811		let tol = eps::<T::Real>() * from_f64::<T::Real>(64.0);
812		let approx_eq = CwiseMat(ApproxEq {
813			abs_tol: tol.copy(),
814			rel_tol: tol,
815		});
816		let n = a.nrows();
817		let run = |mut par: Par| {
818			let par_shape = par.shape();
819			let mut LU = a.cloned();
820			let perm = &mut *vec![0usize; n];
821			let perm_inv = &mut *vec![0usize; n];
822			let params = PartialPivLuParams {
823				recursion_threshold: 2,
824				block_size: 2,
825				..auto!(T)
826			};
827			let p = lu_in_place(
828				LU.as_mut(),
829				perm,
830				perm_inv,
831				par.reborrow(),
832				MemStack::new(&mut MemBuffer::new(lu_in_place_scratch::<
833					usize,
834					T,
835				>(
836					n, n, par_shape, params.into()
837				))),
838				params.into(),
839			)
840			.1;
841			assert!(LU.as_ref().is_all_finite());
842			let mut L = LU.as_ref().cloned();
843			let mut U = LU.as_ref().cloned();
844			for j in 0..n {
845				for i in 0..j {
846					L[(i, j)] = zero();
847				}
848				L[(j, j)] = one();
849			}
850			for j in 0..n {
851				for i in j + 1..n {
852					U[(i, j)] = zero();
853				}
854			}
855			assert!(p.inverse() * L.as_ref() * U.as_ref() ~ a);
856			if let Some(ref x) = l10 {
857				assert!(
858					(L[(1, 0)].copy() - x.copy()).abs()
859						<= eps::<T::Real>() * from_f64::<T::Real>(4.0)
860				);
861			}
862		};
863		run(Par::Seq);
864		crate::team::with_team(8, run);
865	}
866}