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