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