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