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}
86pub(crate) fn lu_in_place_recursion<I: Index, T: ComplexField>(
87	A: MatMut<'_, T>,
88	start: usize,
89	end: usize,
90	trans: &mut [I],
91	par: Par,
92	params: Spec<PartialPivLuParams, T>,
93) -> usize {
94	let params = params.config;
95	let mut A = A;
96	let m = A.nrows();
97	let ncols = A.ncols();
98	let n = end - start;
99	if n <= params.recursion_threshold {
100		return lu_in_place_unblocked(A, start, end, trans);
101	}
102	let half = n / 2;
103	let pow = Ord::min(16, half.next_power_of_two());
104	let block_size = half.next_multiple_of(pow);
105	let mut n_trans = 0;
106	assert!(n <= m);
107	n_trans += lu_in_place_recursion(
108		A.rb_mut().get_mut(.., start..end),
109		0,
110		block_size,
111		&mut trans[..block_size],
112		par,
113		params.into(),
114	);
115	{
116		let mut A = A.rb_mut().get_mut(.., start..end);
117		let (A00, mut A01, A10, mut A11) =
118			A.rb_mut().split_at_mut(block_size, block_size);
119		let A00 = A00.rb();
120		let A10 = A10.rb();
121		// The panel solve and the trailing product below are this level's whole
122		// parallel work, and the block size halves every level of the recursion, so
123		// past a few levels they are a fork and a join around a handful of thousands
124		// of flops. Below the work floor the swap stage already uses, take
125		// `Par::Seq`; the recursion keeps the caller's `par`, so every level decides
126		// for its own trailing update. The crossover comes from a sweep over 256x256,
127		// 512x512 and 1024x1024 threaded `f64`, not from one shape, so it is a
128		// statement about *flops*: `par_threshold` counts `f64` elements, and a
129		// complex element carries four times the arithmetic. Comparing raw areas
130		// made the floor four times too high for `c64`, which left its `128x128`
131		// trailing update sequential at the size where the `f64` one is already
132		// threaded. The comparison saturates, so `usize::MAX` still disables it.
133		let work = A11
134			.nrows()
135			.saturating_mul(A11.ncols())
136			.saturating_mul(if T::IS_REAL { 2 } else { 8 });
137		let par_here = if work > params.par_threshold.saturating_mul(2) {
138			par
139		} else {
140			Par::Seq
141		};
142		{
143			linalg::triangular_solve::solve_unit_lower_triangular_in_place(
144				A00.rb(),
145				A01.rb_mut(),
146				par_here,
147			);
148		}
149		linalg::matmul::matmul(
150			A11.rb_mut(),
151			Accum::Add,
152			A10.rb(),
153			A01.rb(),
154			-one::<T>(),
155			par_here,
156		);
157		n_trans += lu_in_place_recursion(
158			A.rb_mut().get_mut(block_size..m, ..),
159			block_size,
160			n,
161			&mut trans[block_size..n],
162			par,
163			params.into(),
164		);
165	}
166	let swap = |mat: MatMut<'_, T>| {
167		let mut mat = mat;
168		for j in 0..mat.ncols() {
169			let mut col = mat.rb_mut().col_mut(j);
170			if col.row_stride() == 1 {
171				for (j, &t) in trans[..n].iter().enumerate() {
172					swap_elems(col.rb_mut(), j, t.zx() + j);
173				}
174			} else {
175				for (j, &t) in trans[..n].iter().enumerate() {
176					swap_elems(col.rb_mut(), j, t.zx() + j);
177				}
178			}
179		}
180	};
181	let (A_left, A_right) = A.rb_mut().split_at_col_mut(start);
182	let A_right = A_right.get_mut(.., end - start..ncols - start);
183	let par = if m * (ncols - n) > params.par_threshold {
184		par
185	} else {
186		Par::Seq
187	};
188	match par {
189		Par::Seq => {
190			swap(A_left);
191			swap(A_right);
192		},
193		#[cfg(feature = "rayon")]
194		Par::Rayon(nthreads) => {
195			let nthreads = nthreads.get();
196			let len = (A_left.ncols() + A_right.ncols()) as f64;
197			let left_threads = Ord::min(
198				(nthreads as f64 * (A_left.ncols() as f64 / len)) as usize,
199				nthreads,
200			);
201			let right_threads = nthreads - left_threads;
202			crate::utils::thread::join_raw(
203				|_| {
204					if A_left.ncols() > 0 {
205						spindle::for_each(
206							left_threads,
207							A_left.par_col_partition_mut(left_threads),
208							|A| swap(A),
209						)
210					}
211				},
212				|_| {
213					if A_right.ncols() > 0 {
214						spindle::for_each(
215							right_threads,
216							A_right.par_col_partition_mut(right_threads),
217							|A| swap(A),
218						)
219					}
220				},
221				par,
222			);
223		},
224	}
225	n_trans
226}
227/// $LU$ factorization tuning parameters
228#[derive(Copy, Clone, Debug)]
229pub struct PartialPivLuParams {
230	/// threshold at which the implementation should stop recursing
231	pub recursion_threshold: usize,
232	/// blocking variant step size
233	pub block_size: usize,
234	/// threshold at which size parallelism should be disabled, counted in `f64`
235	/// elements of the submatrix that would be threaded: a complex element is
236	/// four real MACs, so it reaches the same floor at a quarter of the area
237	pub par_threshold: usize,
238	#[doc(hidden)]
239	pub non_exhaustive: NonExhaustive,
240}
241/// information about the resulting $LU$ factorization
242#[derive(Copy, Clone, Debug)]
243pub struct PartialPivLuInfo {
244	/// number of transpositions that were performed, can be used to compute
245	/// the determinant of $P$
246	pub transposition_count: usize,
247}
248/// error in the $LU$ factorization
249#[derive(Copy, Clone, Debug)]
250pub enum LdltError {
251	ZeroPivot { index: usize },
252}
253impl<T: ComplexField> Auto<T> for PartialPivLuParams {
254	#[inline]
255	fn auto() -> Self {
256		Self {
257			recursion_threshold: 16,
258			block_size: 64,
259			par_threshold: 128 * 128,
260			non_exhaustive: NonExhaustive(()),
261		}
262	}
263}
264#[inline]
265pub fn lu_in_place_scratch<I: Index, T: ComplexField>(
266	nrows: usize,
267	ncols: usize,
268	par: Par,
269	params: Spec<PartialPivLuParams, T>,
270) -> StackReq {
271	_ = par;
272	_ = params;
273	StackReq::new::<I>(Ord::min(nrows, ncols))
274}
275pub fn lu_in_place<'out, I: Index, T: ComplexField>(
276	A: MatMut<'_, T>,
277	perm: &'out mut [I],
278	perm_inv: &'out mut [I],
279	par: Par,
280	stack: &mut MemStack,
281	params: Spec<PartialPivLuParams, T>,
282) -> (PartialPivLuInfo, PermRef<'out, I>) {
283	let _ = &params;
284	let truncate = I::truncate;
285	#[cfg(feature = "perf-warn")]
286	if (A.col_stride().unsigned_abs() == 1
287		|| A.row_stride().unsigned_abs() != 1)
288		&& crate::__perf_warn!(LU_WARN)
289	{
290		log::warn!(
291			target : "faer_perf",
292			"LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides."
293		);
294	}
295	let mut matrix = A;
296	let mut stack = stack;
297	let m = matrix.nrows();
298	let n = matrix.ncols();
299	// Row-major input: the unblocked core walks each pivot column twice (the
300	// search, then the scaling) and the swap stage swaps elements inside a
301	// column, which is a cache line per element when the columns are not
302	// contiguous -- `size * m` touches that way against `m * n` elements the copy
303	// moves contiguously twice. Factor a column-major copy and write the factor
304	// back, which is what LAPACK's row-major interface pays internally; the
305	// arithmetic is the copy's, so the factor and the permutation are the
306	// column-major ones. The guard is the panel's own cost model, the same one
307	// `qr_in_place` states: a wide matrix has `size == m` over `n` columns, where
308	// the copy would cost more than it saved.
309	if matrix.row_stride().unsigned_abs() != 1
310		&& matrix.nrows() >= matrix.ncols()
311		&& matrix.ncols() > 1
312		&& linalg::row_major_copy_pays(matrix.nrows(), matrix.ncols())
313	{
314		let mut work = alloc::vec![zero::<T>(); m * n];
315		let mut work = MatMut::from_column_major_slice_mut(&mut work, m, n);
316		linalg::copy_across_layouts(work.rb_mut(), matrix.rb(), par);
317		let ret = lu_in_place(
318			work.rb_mut(),
319			perm,
320			perm_inv,
321			par,
322			stack,
323			params,
324		);
325		linalg::copy_across_layouts(matrix, work.rb(), par);
326		return ret;
327	}
328	let size = Ord::min(n, m);
329	for i in 0..m {
330		let p = &mut perm[i];
331		*p = truncate(i);
332	}
333	let (mut transpositions, _) =
334		stack.rb_mut().make_with(size, |_| truncate(0));
335	let transpositions = transpositions.as_mut();
336	let n_transpositions = lu_in_place_recursion(
337		matrix.rb_mut(),
338		0,
339		size,
340		transpositions.as_mut(),
341		par,
342		params,
343	);
344	for idx in 0..size {
345		let t = transpositions[idx];
346		perm.as_mut().swap(idx, idx + t.zx());
347	}
348	if m < n {
349		let (left, right) = matrix.split_at_col_mut(size);
350		linalg::triangular_solve::solve_unit_lower_triangular_in_place(
351			left.rb(),
352			right,
353			par,
354		);
355	}
356	for i in 0..m {
357		perm_inv[perm[i].zx()] = truncate(i);
358	}
359	(
360		PartialPivLuInfo {
361			transposition_count: n_transpositions,
362		},
363		unsafe { PermRef::new_unchecked(perm, perm_inv, m) },
364	)
365}
366#[cfg(test)]
367mod tests {
368	use super::*;
369	use crate::stats::prelude::*;
370	use crate::utils::approx::*;
371	use crate::{Mat, assert, c32, c64};
372	use dyn_stack::MemBuffer;
373	#[test]
374	fn test_plu() {
375		let rng = &mut StdRng::seed_from_u64(0);
376		let approx_eq = CwiseMat(ApproxEq {
377			abs_tol: 1e-13,
378			rel_tol: 1e-13,
379		});
380		for n in [1, 2, 3, 128, 255, 256, 257] {
381			let A = CwiseMatDistribution {
382				nrows: n,
383				ncols: n,
384				dist: StandardNormal,
385			}
386			.rand::<Mat<f64>>(rng);
387			let A = A.as_ref();
388			let mut LU = A.cloned();
389			let perm = &mut *vec![0usize; n];
390			let perm_inv = &mut *vec![0usize; n];
391			let params = PartialPivLuParams {
392				recursion_threshold: 2,
393				block_size: 2,
394				..auto!(f64)
395			};
396			let p = lu_in_place(
397				LU.as_mut(),
398				perm,
399				perm_inv,
400				Par::Seq,
401				MemStack::new(&mut MemBuffer::new(lu_in_place_scratch::<
402					usize,
403					f64,
404				>(
405					n,
406					n,
407					Par::Seq,
408					params.into(),
409				))),
410				params.into(),
411			)
412			.1;
413			let mut L = LU.as_ref().cloned();
414			let mut U = LU.as_ref().cloned();
415			for j in 0..n {
416				for i in 0..j {
417					L[(i, j)] = 0.0;
418				}
419				L[(j, j)] = 1.0;
420			}
421			for j in 0..n {
422				for i in j + 1..n {
423					U[(i, j)] = 0.0;
424				}
425			}
426			let L = L.as_ref();
427			let U = U.as_ref();
428			assert!(p.inverse() * L * U ~ A);
429		}
430		for m in [8, 128, 255, 256, 257] {
431			let n = 8;
432			let A = CwiseMatDistribution {
433				nrows: m,
434				ncols: n,
435				dist: StandardNormal,
436			}
437			.rand::<Mat<f64>>(rng);
438			let A = A.as_ref();
439			let mut LU = A.cloned();
440			let perm = &mut *vec![0usize; m];
441			let perm_inv = &mut *vec![0usize; m];
442			let p = lu_in_place(
443				LU.as_mut(),
444				perm,
445				perm_inv,
446				Par::Seq,
447				MemStack::new(&mut MemBuffer::new(lu_in_place_scratch::<
448					usize,
449					f64,
450				>(
451					n, n, Par::Seq, default()
452				))),
453				default(),
454			)
455			.1;
456			let mut L = LU.as_ref().cloned();
457			let mut U = LU.as_ref().cloned();
458			for j in 0..n {
459				for i in 0..j {
460					L[(i, j)] = 0.0;
461				}
462				L[(j, j)] = 1.0;
463			}
464			for j in 0..n {
465				for i in j + 1..m {
466					U[(i, j)] = 0.0;
467				}
468			}
469			let L = L.as_ref();
470			let U = U.as_ref();
471			let U = U.subrows(0, n);
472			assert!(p.inverse() * L * U ~ A);
473		}
474	}
475	fn factor_lu(A: &Mat<f64>, par: Par) -> (Mat<f64>, Vec<usize>, usize) {
476		let n = A.nrows();
477		let mut LU = A.cloned();
478		let perm = &mut *vec![0usize; n];
479		let perm_inv = &mut *vec![0usize; n];
480		let params: Spec<PartialPivLuParams, f64> = default();
481		let stack =
482			&mut MemBuffer::new(lu_in_place_scratch::<usize, f64>(n, n, par, params));
483		let stack = MemStack::new(stack);
484		let (info, _) = lu_in_place(LU.as_mut(), perm, perm_inv, par, stack, params);
485		(LU, perm.to_vec(), info.transposition_count)
486	}
487	fn factor_lu_c64(A: &Mat<c64>, par: Par) -> (Mat<c64>, Vec<usize>, usize) {
488		let n = A.nrows();
489		let mut LU = A.cloned();
490		let perm = &mut *vec![0usize; n];
491		let perm_inv = &mut *vec![0usize; n];
492		let params: Spec<PartialPivLuParams, c64> = default();
493		let stack =
494			&mut MemBuffer::new(lu_in_place_scratch::<usize, c64>(n, n, par, params));
495		let stack = MemStack::new(stack);
496		let (info, _) = lu_in_place(LU.as_mut(), perm, perm_inv, par, stack, params);
497		(LU, perm.to_vec(), info.transposition_count)
498	}
499	/// The two products at a recursion level take `Par::Seq` while the trailing update's work is
500	/// below the floor, instead of the caller's hint. That is a choice of how to schedule the
501	/// same arithmetic, so both sides of the floor have to return the same factorization: `96`,
502	/// `128` and `192` are below `128 * 128` `f64` elements at the top level, `256` and `512`
503	/// above it, and every level in between halves down through it. A `c64` element is four real
504	/// MACs, so the same floor is reached at a quarter of the area -- `128` is below it and `256`
505	/// above it for `c64` too, which is the case the element-only comparison got wrong.
506	#[test]
507	fn threaded_lu_matches_the_sequential_one() {
508		let rng = &mut StdRng::seed_from_u64(0);
509		let pool = rayon::ThreadPoolBuilder::new()
510			.num_threads(4)
511			.build()
512			.unwrap();
513		let approx_eq = CwiseMat(ApproxEq {
514			abs_tol: 1e-12,
515			rel_tol: 1e-12,
516		});
517		for n in [96, 128, 192, 256, 512] {
518			let A = CwiseMatDistribution {
519				nrows: n,
520				ncols: n,
521				dist: StandardNormal,
522			}
523			.rand::<Mat<f64>>(rng);
524			let (seq, perm_seq, trans_seq) = factor_lu(&A, Par::Seq);
525			let (par, perm_par, trans_par) =
526				pool.install(|| factor_lu(&A, Par::rayon(4)));
527			assert!(seq.as_ref() ~ par.as_ref());
528			assert!(perm_seq == perm_par);
529			assert!(trans_seq == trans_par);
530		}
531		for n in [96, 128, 192, 256, 512] {
532			let real = CwiseMatDistribution {
533				nrows: n,
534				ncols: n,
535				dist: StandardNormal,
536			}
537			.rand::<Mat<f64>>(rng);
538			let imag = CwiseMatDistribution {
539				nrows: n,
540				ncols: n,
541				dist: StandardNormal,
542			}
543			.rand::<Mat<f64>>(rng);
544			let A = Mat::<c64>::from_fn(n, n, |i, j| c64::new(real[(i, j)], imag[(i, j)]));
545			let (seq, perm_seq, trans_seq) = factor_lu_c64(&A, Par::Seq);
546			let (par, perm_par, trans_par) =
547				pool.install(|| factor_lu_c64(&A, Par::rayon(4)));
548			for j in 0..n {
549				for i in 0..n {
550					let diff = (seq[(i, j)] - par[(i, j)]).abs();
551					assert!(diff <= 1e-10, "({i}, {j}) differs by {diff:e}");
552				}
553			}
554			assert!(perm_seq == perm_par);
555			assert!(trans_seq == trans_par);
556		}
557	}
558	/// An exactly zero pivot column is a singular matrix, not a failed factorization: the
559	/// factors stay finite and still reconstruct the input, with the singularity left in `U`.
560	/// A subnormal pivot must not be scaled by its infinite reciprocal either. OpenBLAS's
561	/// `getf2_k` skips the column when the pivot is exactly zero, and reference `dgetf2`
562	/// divides elementwise below `SFMIN`.
563	#[test]
564	fn zero_pivot_column_reconstructs() {
565		fn zero_pivot<T: ComplexField>(n: usize) -> Mat<T> {
566			Mat::<T>::from_fn(n, n, |i, j| {
567				if j > 0 {
568					from_f64::<T>((i + 2 * j) as f64)
569				} else {
570					zero()
571				}
572			})
573		}
574		// the pivot is the largest entry of a column, so a subnormal pivot needs a
575		// subnormal fixture: `tiny` is the smallest useful one for each dtype
576		fn subnormal<T: ComplexField>(tiny: f64) -> Mat<T> {
577			let tiny = from_f64::<T>(tiny);
578			Mat::<T>::from_fn(2, 2, |i, j| match (i, j) {
579				(0, 0) => tiny.copy(),
580				(0, 1) => one(),
581				(1, 0) => tiny.mul_real(from_f64::<T::Real>(0.5)),
582				(1, 1) => from_f64::<T>(2.0),
583				_ => zero(),
584			})
585		}
586		for n in [1usize, 2, 3, 128] {
587			check_factor::<f32>(zero_pivot::<f32>(n).as_ref(), None);
588			check_factor::<c32>(zero_pivot::<c32>(n).as_ref(), None);
589			check_factor::<f64>(zero_pivot::<f64>(n).as_ref(), None);
590			check_factor::<c64>(zero_pivot::<c64>(n).as_ref(), None);
591		}
592		// a power of two so that the subnormal and its half are both exact and the
593		// expected multiplier is exactly `0.5`
594		check_factor::<f32>(
595			subnormal::<f32>(2f64.powi(-147)).as_ref(),
596			Some(0.5f32),
597		);
598		check_factor::<c32>(
599			subnormal::<c32>(2f64.powi(-147)).as_ref(),
600			Some(c32::new(0.5, 0.0)),
601		);
602		check_factor::<f64>(
603			subnormal::<f64>(2f64.powi(-1072)).as_ref(),
604			Some(0.5f64),
605		);
606		check_factor::<c64>(
607			subnormal::<c64>(2f64.powi(-1072)).as_ref(),
608			Some(c64::new(0.5, 0.0)),
609		);
610	}
611	fn check_factor<T: ComplexField>(a: MatRef<'_, T>, l10: Option<T>) {
612		let tol = eps::<T::Real>() * from_f64::<T::Real>(64.0);
613		let approx_eq = CwiseMat(ApproxEq {
614			abs_tol: tol.copy(),
615			rel_tol: tol,
616		});
617		let n = a.nrows();
618		for par in [Par::Seq, Par::rayon(8)] {
619			let mut LU = a.cloned();
620			let perm = &mut *vec![0usize; n];
621			let perm_inv = &mut *vec![0usize; n];
622			let params = PartialPivLuParams {
623				recursion_threshold: 2,
624				block_size: 2,
625				..auto!(T)
626			};
627			let p = lu_in_place(
628				LU.as_mut(),
629				perm,
630				perm_inv,
631				par,
632				MemStack::new(&mut MemBuffer::new(lu_in_place_scratch::<
633					usize,
634					T,
635				>(
636					n, n, par, params.into()
637				))),
638				params.into(),
639			)
640			.1;
641			assert!(LU.as_ref().is_all_finite());
642			let mut L = LU.as_ref().cloned();
643			let mut U = LU.as_ref().cloned();
644			for j in 0..n {
645				for i in 0..j {
646					L[(i, j)] = zero();
647				}
648				L[(j, j)] = one();
649			}
650			for j in 0..n {
651				for i in j + 1..n {
652					U[(i, j)] = zero();
653				}
654			}
655			assert!(p.inverse() * L.as_ref() * U.as_ref() ~ a);
656			if let Some(ref x) = l10 {
657				assert!(
658					(L[(1, 0)].copy() - x.copy()).abs()
659						<= eps::<T::Real>() * from_f64::<T::Real>(4.0)
660				);
661			}
662		}
663	}
664}