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