Skip to main content

faer/linalg/qr/col_pivoting/
factor.rs

1pub use super::super::no_pivoting::factor::recommended_block_size;
2use crate::assert;
3use crate::internal_prelude::*;
4use crate::perm::swap_cols_idx;
5use linalg::householder::{self, HouseholderInfo};
6use pulp::Simd;
7fn update_mat_and_dot_simd<T: ComplexField>(
8	norm: RowMut<'_, T>,
9	dot: RowMut<'_, T>,
10	B01: RowMut<'_, T>,
11	B11: MatMut<'_, T, usize, usize, ContiguousFwd>,
12	A10: ColRef<'_, T, usize, ContiguousFwd>,
13	B10: ColRef<'_, T, usize, ContiguousFwd>,
14	l: T,
15	tau_inv: T::Real,
16	align: usize,
17) {
18	struct Impl<'a, 'M, 'N, T: ComplexField> {
19		norm: RowMut<'a, T, Dim<'N>>,
20		dot: RowMut<'a, T, Dim<'N>>,
21		B01: RowMut<'a, T, Dim<'N>>,
22		B11: MatMut<'a, T, Dim<'M>, Dim<'N>, ContiguousFwd>,
23		A10: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
24		B10: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
25		l: T,
26		tau_inv: T::Real,
27		align: usize,
28	}
29	impl<'M, 'N, T: ComplexField> pulp::WithSimd for Impl<'_, 'M, 'N, T> {
30		type Output = ();
31
32		#[inline(always)]
33		fn with_simd<S: Simd>(self, simd: S) -> Self::Output {
34			let Self {
35				mut norm,
36				mut dot,
37				B01: mut u,
38				mut B11,
39				A10,
40				B10,
41				l,
42				ref tau_inv,
43				align,
44			} = self;
45			let m = B11.nrows();
46			let n = B11.ncols();
47			let simd =
48				SimdCtx::<'_, T, S>::new_align(T::simd_ctx(simd), m, align);
49			let indices4 = simd.batch_indices::<4>();
50
51			for j in n.indices() {
52				let b0 = dot[j].copy();
53				let rhs0 = simd.splat(&b0);
54				let mut acc = [simd.zero(); 4];
55				simd_iter!(for (IDX, i) in [indices4; 4] {
56					let lhs0 = simd.read(A10, i);
57					let lhs1 = simd.read(B10, i);
58					let mut dst0 = simd.read(B11.rb().col(j), i);
59					dst0 = simd.mul_add(lhs0, rhs0, dst0);
60					acc[IDX] = simd.conj_mul_add(lhs1, dst0, acc[IDX]);
61					simd.write(B11.rb_mut().col_mut(j), i, dst0);
62				});
63				let acc = simd
64					.add(simd.add(acc[0], acc[2]), simd.add(acc[1], acc[3]));
65				let tmp = &u[j] + &l * b0;
66				let d0 = (&tmp + simd.reduce_sum(acc)).mul_real(-tau_inv);
67				u[j] = tmp + &d0;
68				dot[j] = d0;
69				norm[j] = (norm[j].abs2() - u[j].abs2()).sqrt().to_cplx();
70			}
71		}
72	}
73	with_dim!(M, B11.nrows());
74	with_dim!(N, B11.ncols());
75	dispatch!(
76		Impl {
77			norm: norm.as_col_shape_mut(N),
78			dot: dot.as_col_shape_mut(N),
79			B01: B01.as_col_shape_mut(N),
80			B11: B11.as_shape_mut(M, N),
81			A10: A10.as_row_shape(M),
82			B10: B10.as_row_shape(M),
83			l,
84			tau_inv,
85			align
86		},
87		Impl,
88		T
89	)
90}
91/// $QR$ factorization with column pivoting tuning parameters
92#[derive(Copy, Clone, Debug)]
93pub struct ColPivQrParams {
94	/// threshold at which blocking algorithms should be disabled
95	pub blocking_threshold: usize,
96	/// threshold at which the parallelism should be disabled
97	pub par_threshold: usize,
98	#[doc(hidden)]
99	pub non_exhaustive: NonExhaustive,
100}
101impl<T: ComplexField> Auto<T> for ColPivQrParams {
102	#[inline]
103	fn auto() -> Self {
104		Self {
105			blocking_threshold: 48 * 48,
106			par_threshold: 192 * 256,
107			non_exhaustive: NonExhaustive(()),
108		}
109	}
110}
111#[track_caller]
112fn qr_in_place_unblocked<'out, I: Index, T: ComplexField>(
113	A: MatMut<'_, T>,
114	H: RowMut<'_, T>,
115	col_perm: &'out mut [I],
116	col_perm_inv: &'out mut [I],
117	par: Par,
118	stack: &mut MemStack,
119	params: Spec<ColPivQrParams, T>,
120) -> (ColPivQrInfo, PermRef<'out, I>) {
121	let m = A.nrows();
122	let n = A.ncols();
123	let size = H.ncols();
124	let params = params.config;
125	let mut A = A;
126	let mut H = H;
127	let mut par = par;
128	assert!(size == Ord::min(m, n));
129	for j in 0..n {
130		col_perm[j] = I::truncate(j);
131	}
132	let mut n_trans = 0;
133	'main: {
134		if size == 0 {
135			break 'main;
136		}
137		let (mut dot, stack) = temp_mat_zeroed::<T, _, _>(n, 1, stack);
138		let (mut norm, stack) = temp_mat_zeroed::<T, _, _>(n, 1, stack);
139		let _ = stack;
140		let mut dot = dot.as_mat_mut().col_mut(0).transpose_mut();
141		let mut norm = norm.as_mat_mut().col_mut(0).transpose_mut();
142		let mut best = zero();
143		let ref threshold = eps::<T::Real>().sqrt();
144		for j in 0..n {
145			let val = A.rb().col(j).norm_l2();
146			norm[j] = val.to_cplx();
147			if val > best {
148				best = val;
149			}
150		}
151		// scaling by the largest column norm keeps the reduction in range; a zero
152		// matrix has nothing to scale, and LAPACK's `dlarfg` makes an all-zero column
153		// the identity reflector rather than a division by its norm.
154		let ref scale_fwd = if best > zero() { best.copy() } else { one() };
155		let ref scale_bwd = scale_fwd.recip();
156		zip!(A.rb_mut()).for_each(|unzip!(a)| *a = a.mul_real(scale_bwd));
157		for j in 0..n {
158			norm[j] = (norm[j].real() * scale_bwd).to_cplx();
159		}
160		best = best * scale_bwd;
161		let mut best_threshold = best * threshold;
162		'unscale: {
163			for k in 0..size {
164				let mut new_best = zero::<T::Real>();
165				let mut best_col = k;
166				for j in k..n {
167					let val = norm[j].real();
168					if val > new_best {
169						new_best = val;
170						best_col = j;
171					}
172				}
173				let delayed_update = T::SIMD_CAPABILITIES.is_simd()
174					&& A.row_stride() == 1
175					&& k > 0 && new_best >= best_threshold;
176				if k > 0 && !delayed_update {
177					let (_, _, A10, mut A11) = A.rb_mut().split_at_mut(k, k);
178					let dot = dot.rb().get(k..);
179					let A10 = A10.rb().col(k - 1);
180					linalg::matmul::matmul(
181						A11.rb_mut(),
182						Accum::Add,
183						A10,
184						dot,
185						one(),
186						par,
187					);
188					best = zero();
189					for j in k..n {
190						let val = A11.rb().col(j - k).norm_l2();
191						norm[j] = val.to_cplx();
192						if val > best {
193							best = val;
194							best_col = j;
195						}
196					}
197					best_threshold = best * threshold;
198				}
199				if best_col != k {
200					n_trans += 1;
201					col_perm.as_mut().swap(best_col, k);
202					swap_cols_idx(A.rb_mut(), best_col, k);
203					swap_cols_idx(dot.rb_mut().as_mat_mut(), best_col, k);
204					swap_cols_idx(norm.rb_mut().as_mat_mut(), best_col, k);
205				}
206				let (_, _, A10, mut A11) = A.rb_mut().split_at_mut(k, k);
207				let A10 = A10.rb();
208				let dot0 = dot.rb_mut().get_mut(k..);
209				let (mut B00, B01, B10, mut B11) =
210					A11.rb_mut().split_at_mut(1, 1);
211				let B00 = &mut B00[(0, 0)];
212				let mut B01 = B01.row_mut(0);
213				let mut B10 = B10.col_mut(0);
214				let l = if delayed_update {
215					let A10 = A10.col(k - 1);
216					A10[0].copy()
217				} else {
218					zero()
219				};
220				let r = dot0[0].copy();
221				let mut dot = dot.rb_mut().get_mut(k + 1..);
222				let mut norm = norm.rb_mut().get_mut(k + 1..);
223				if delayed_update {
224					let A10 = A10.col(k - 1).get(1..);
225					*B00 += &l * &r;
226					zip!(B10.rb_mut(), A10).for_each(|unzip!(x, y)| {
227						*x += &r * y;
228					});
229				}
230				let HouseholderInfo { tau, .. } =
231					householder::make_householder_in_place(B00, B10.rb_mut());
232				let ref tau_inv = tau.recip();
233				H[k] = tau.to_cplx();
234				if k + 1 == size {
235					if delayed_update {
236						zip!(B01.rb_mut(), dot.rb()).for_each(
237							|unzip!(x, y)| {
238								*x += &l * y;
239							},
240						);
241					}
242					break 'unscale;
243				}
244				if (m - k - 1) * (n - k - 1) < params.par_threshold {
245					par = Par::Seq;
246				}
247				if delayed_update {
248					let A10 = A10.col(k - 1).get(1..);
249					match par {
250						Par::Seq => {
251							update_mat_and_dot_simd(
252								norm.rb_mut(),
253								dot.rb_mut(),
254								B01.rb_mut(),
255								B11.rb_mut().try_as_col_major_mut().unwrap(),
256								A10.try_as_col_major().unwrap(),
257								B10.rb().try_as_col_major().unwrap(),
258								l.copy(),
259								tau_inv.copy(),
260								simd_align(k + 1),
261							);
262						},
263						#[cfg(feature = "rayon")]
264						Par::Rayon(nthreads) => {
265							let nthreads = nthreads.get();
266							use rayon::prelude::*;
267							spindle::for_each(
268								nthreads,
269								norm.par_partition_mut(nthreads)
270									.zip(dot.par_partition_mut(nthreads))
271									.zip(B01.par_partition_mut(nthreads))
272									.zip(B11.par_col_partition_mut(nthreads)),
273								|(((norm, dot), B01), B11)| {
274									update_mat_and_dot_simd(
275										norm,
276										dot,
277										B01,
278										B11.try_as_col_major_mut().unwrap(),
279										A10.try_as_col_major().unwrap(),
280										B10.rb().try_as_col_major().unwrap(),
281										l.copy(),
282										tau_inv.copy(),
283										simd_align(k + 1),
284									);
285								},
286							);
287						},
288					}
289				} else {
290					dot.copy_from(B01.rb());
291					linalg::matmul::matmul(
292						dot.rb_mut(),
293						Accum::Add,
294						B10.rb().adjoint(),
295						B11.rb(),
296						one(),
297						par,
298					);
299					zip!(B01.rb_mut(), dot.rb_mut(), norm.rb_mut()).for_each(
300						|unzip!(a, dot, norm)| {
301							*dot = -dot.mul_real(tau_inv);
302							*a += &*dot;
303							*norm = (norm.abs2() - a.abs2()).sqrt().to_cplx();
304						},
305					);
306				}
307			}
308		}
309		zip!(A.rb_mut()).for_each_triangular_upper(
310			linalg::zip::Diag::Include,
311			|unzip!(a)| *a = a.mul_real(scale_fwd),
312		);
313	}
314	for j in 0..n {
315		col_perm_inv[col_perm[j].zx()] = I::truncate(j);
316	}
317	(
318		ColPivQrInfo {
319			transposition_count: n_trans,
320		},
321		unsafe { PermRef::new_unchecked(col_perm, col_perm_inv, n) },
322	)
323}
324/// computes the layout of required workspace for performing a qr decomposition
325/// with column pivoting
326pub fn qr_in_place_scratch<I: Index, T: ComplexField>(
327	nrows: usize,
328	ncols: usize,
329	block_size: usize,
330	par: Par,
331	params: Spec<ColPivQrParams, T>,
332) -> StackReq {
333	let _ = nrows;
334	let _ = ncols;
335	let _ = par;
336	let _ = block_size;
337	let _ = &params;
338	linalg::temp_mat_scratch::<T>(ncols, 2)
339}
340/// information about the resulting $QR$ factorization.
341#[derive(Copy, Clone, Debug)]
342pub struct ColPivQrInfo {
343	/// number of transpositions that were performed, can be used to compute
344	/// the determinant of $P$.
345	pub transposition_count: usize,
346}
347#[track_caller]
348pub fn qr_in_place<'out, I: Index, T: ComplexField>(
349	A: MatMut<'_, T>,
350	Q_coeff: MatMut<'_, T>,
351	col_perm: &'out mut [I],
352	col_perm_inv: &'out mut [I],
353	par: Par,
354	stack: &mut MemStack,
355	params: Spec<ColPivQrParams, T>,
356) -> (ColPivQrInfo, PermRef<'out, I>) {
357	// Row-major input: the pivot search walks every one of the `n` columns of
358	// length `m` (`for j in 0..n { A.col(j).norm_l2() }`, and again per step when
359	// the incremental-norm path is closed), and that path is column-major only --
360	// it is gated on `A.row_stride() == 1` and unwraps a column-major view. So a
361	// row-major matrix touches `m * n` cache lines where the copy below moves
362	// `m * n` elements contiguously twice, on every shape rather than only on the
363	// tall ones. Factor a column-major copy and write the factor back, which is
364	// what LAPACK's row-major interface pays internally; the arithmetic is the
365	// copy's, so the factor, the `tau` matrix and the permutations are the
366	// column-major ones.
367	let mut A = A;
368	if A.row_stride().unsigned_abs() != 1
369		&& A.nrows() > 1
370		&& A.ncols() > 1
371		&& linalg::row_major_copy_pays(A.nrows(), A.ncols())
372	{
373		let (m, n) = A.shape();
374		let mut work = alloc::vec![zero::<T>(); m * n];
375		let mut work = MatMut::from_column_major_slice_mut(&mut work, m, n);
376		linalg::copy_across_layouts(work.rb_mut(), A.rb(), par);
377		let ret = qr_in_place(
378			work.rb_mut(),
379			Q_coeff,
380			col_perm,
381			col_perm_inv,
382			par,
383			stack,
384			params,
385		);
386		linalg::copy_across_layouts(A, work.rb(), par);
387		return ret;
388	}
389	let mut H = Q_coeff;
390	let size = H.ncols();
391	let block_size = H.nrows();
392	let ret = qr_in_place_unblocked(
393		A.rb_mut(),
394		H.rb_mut().row_mut(0),
395		col_perm,
396		col_perm_inv,
397		par,
398		stack,
399		params,
400	);
401	let mut j = 0;
402	while j < size {
403		let block_size = Ord::min(block_size, size - j);
404		let mut H = H
405			.rb_mut()
406			.subcols_mut(j, block_size)
407			.subrows_mut(0, block_size);
408		for j in 0..block_size {
409			H[(j, j)] = H[(0, j)].copy();
410		}
411		let A = A.rb().get(j.., j..j + block_size);
412		householder::upgrade_householder_factor(
413			H.rb_mut(),
414			A,
415			block_size,
416			1,
417			par,
418		);
419		j += block_size;
420	}
421	ret
422}
423#[cfg(test)]
424mod tests {
425	use super::*;
426	use crate::stats::prelude::*;
427	use crate::utils::approx::*;
428	use crate::{Mat, assert, c32, c64};
429	use dyn_stack::MemBuffer;
430	#[test]
431	fn test_unblocked_qr() {
432		let rng = &mut StdRng::seed_from_u64(0);
433		for par in [Par::Seq, Par::rayon(8)] {
434			for n in [2, 3, 4, 8, 16, 24, 32, 128, 255] {
435				let bs = 15;
436				let approx_eq = CwiseMat(ApproxEq {
437					abs_tol: 1e-10,
438					rel_tol: 1e-10,
439				});
440				let A = CwiseMatDistribution {
441					nrows: n,
442					ncols: n,
443					dist: ComplexDistribution::new(
444						StandardNormal,
445						StandardNormal,
446					),
447				}
448				.rand::<Mat<c64>>(rng);
449				let A = A.as_ref();
450				let mut QR = A.cloned();
451				let mut H = Mat::zeros(bs, n);
452				let col_perm = &mut *vec![0usize; n];
453				let col_perm_inv = &mut *vec![0usize; n];
454				let q = qr_in_place(
455					QR.as_mut(),
456					H.as_mut(),
457					col_perm,
458					col_perm_inv,
459					par,
460					MemStack::new(&mut MemBuffer::new(qr_in_place_scratch::<
461						usize,
462						c64,
463					>(
464						n,
465						n,
466						bs,
467						par,
468						default(),
469					))),
470					default(),
471				)
472				.1;
473				let mut Q = Mat::<c64, _, _>::zeros(n, n);
474				let mut R = QR.as_ref().cloned();
475				for j in 0..n {
476					Q[(j, j)] = c64::ONE;
477				}
478				householder::apply_block_householder_sequence_on_the_left_in_place_with_conj(
479					QR.as_ref(),
480					H.as_ref(),
481					Conj::No,
482					Q.as_mut(),
483					Par::Seq,
484					MemStack::new(&mut MemBuffer::new(
485						householder::apply_block_householder_sequence_on_the_left_in_place_scratch::<c64>(n, bs, n),
486					)),
487				);
488				for j in 0..n {
489					for i in j + 1..n {
490						R[(i, j)] = c64::ZERO;
491					}
492				}
493				assert!(Q * R * q ~ A);
494			}
495			let n = 20;
496			for m in [2, 3, 4, 8, 16, 24, 32, 128, 255] {
497				let bs = 15;
498				let size = Ord::min(m, n);
499				let approx_eq = CwiseMat(ApproxEq {
500					abs_tol: 1e-10,
501					rel_tol: 1e-10,
502				});
503				let A = CwiseMatDistribution {
504					nrows: m,
505					ncols: n,
506					dist: ComplexDistribution::new(
507						StandardNormal,
508						StandardNormal,
509					),
510				}
511				.rand::<Mat<c64>>(rng);
512				let A = A.as_ref();
513				let mut QR = A.cloned();
514				let mut H = Mat::zeros(bs, size);
515				let col_perm = &mut *vec![0usize; n];
516				let col_perm_inv = &mut *vec![0usize; n];
517				let q = qr_in_place(
518					QR.as_mut(),
519					H.as_mut(),
520					col_perm,
521					col_perm_inv,
522					par,
523					MemStack::new(&mut MemBuffer::new(qr_in_place_scratch::<
524						usize,
525						c64,
526					>(
527						m,
528						n,
529						bs,
530						par,
531						default(),
532					))),
533					default(),
534				)
535				.1;
536				let mut Q = Mat::<c64, _, _>::zeros(m, m);
537				let mut R = QR.as_ref().cloned();
538				for j in 0..m {
539					Q[(j, j)] = c64::ONE;
540				}
541				householder::apply_block_householder_sequence_on_the_left_in_place_with_conj(
542					QR.as_ref().subcols(0, size),
543					H.as_ref(),
544					Conj::No,
545					Q.as_mut(),
546					Par::Seq,
547					MemStack::new(&mut MemBuffer::new(
548						householder::apply_block_householder_sequence_on_the_left_in_place_scratch::<c64>(m, bs, m),
549					)),
550				);
551				for j in 0..n {
552					for i in j + 1..m {
553						R[(i, j)] = c64::ZERO;
554					}
555				}
556				assert!(Q * R * q ~ A);
557			}
558		}
559	}
560	/// A nonempty zero matrix has a valid factorization -- zero `R` and a unitary `Q` --
561	/// so scaling by the largest column norm must not divide by a zero pivot scale.
562	#[test]
563	fn zero_matrix_reconstructs() {
564		// 64x64 crosses the blocking threshold, the rest keep the panel path short
565		for (m, n) in [(1, 1), (3, 2), (8, 8), (64, 64)] {
566			for par in [Par::Seq, Par::rayon(8)] {
567				check_zero_matrix::<f32>(m, n, par);
568				check_zero_matrix::<c32>(m, n, par);
569				check_zero_matrix::<f64>(m, n, par);
570				check_zero_matrix::<c64>(m, n, par);
571			}
572		}
573	}
574	fn check_zero_matrix<T: ComplexField>(m: usize, n: usize, par: Par) {
575		let size = Ord::min(m, n);
576		let bs = 15;
577		let approx_eq = CwiseMat(ApproxEq {
578			abs_tol: from_f64::<T::Real>(1e-10),
579			rel_tol: from_f64::<T::Real>(1e-10),
580		});
581		let a = Mat::<T>::zeros(m, n);
582		let a = a.as_ref();
583		let mut QR = a.cloned();
584		let mut H = Mat::<T>::zeros(bs, size);
585		let col_perm = &mut *vec![0usize; n];
586		let col_perm_inv = &mut *vec![0usize; n];
587		let perm = qr_in_place(
588			QR.as_mut(),
589			H.as_mut(),
590			col_perm,
591			col_perm_inv,
592			par,
593			MemStack::new(&mut MemBuffer::new(
594				qr_in_place_scratch::<usize, T>(m, n, bs, par, default()),
595			)),
596			default(),
597		)
598		.1;
599		assert!(QR.as_ref().is_all_finite());
600		assert!(QR.as_ref().norm_max() == zero::<T::Real>());
601		let mut q = Mat::<T, _, _>::zeros(m, m);
602		for j in 0..m {
603			q[(j, j)] = one();
604		}
605		householder::apply_block_householder_sequence_on_the_left_in_place_with_conj(
606			QR.as_ref().subcols(0, size),
607			H.as_ref(),
608			Conj::No,
609			q.as_mut(),
610			Par::Seq,
611			MemStack::new(&mut MemBuffer::new(
612				householder::apply_block_householder_sequence_on_the_left_in_place_scratch::<T>(m, bs, m),
613			)),
614		);
615		// `R` is exactly zero, so the factor itself stands for it -- which makes the
616		// reconstruction true for any finite `Q`, so `Q`'s unitarity is checked separately
617		assert!(
618			(q.adjoint() * &q - Mat::<T>::identity(m, m)).norm_max()
619				<= eps::<T::Real>() * from_f64::<T::Real>(32.0)
620		);
621		assert!(q * QR.as_ref() * perm ~ a);
622	}
623}