Skip to main content

faer/linalg/svd/
bidiag.rs

1use crate::assert;
2use crate::internal_prelude::*;
3use linalg::householder::*;
4use crate::utils::cost::{ParThreshold, rate};
5use crate::utils::thread::with_width;
6use linalg::matmul::{dot, matmul};
7
8/// width of one blocked panel: `BLOCK_SIZE` columns and rows are reduced with level-2 work
9/// against deferred corrections, then the trailing block is brought up to date once with two
10/// level-3 products: the block reduction of Dongarra, Hammarling and Sorensen, *Block reduction of
11/// matrices to condensed forms for eigenvalue computations* (J. Comput. Appl. Math. 27, 1989), the
12/// algorithm LAPACK's `dlabrd`/`dgebrd` implement. the recurrences here are derived from this
13/// crate's reflector conventions, not translated from any implementation.
14///
15/// measured (`docs/worklogs/2026-10-23-blocked-bidiag.md`): the per-step recurrence costs grow
16/// with the width while the update's cost shrinks with it, and 8 to 16 is flat at every size
17/// and budget tried, with 32 and 48 slower.
18const BLOCK_SIZE: usize = 16;
19/// the blocked loop runs while the remaining dimension `min(m - p, n - p)` exceeds this floor;
20/// the tail below it, and every input with `min(m, n) <= BLOCK_FLOOR`, keeps the unblocked
21/// per-step update exactly as it was.
22const BLOCK_FLOOR: usize = 128;
23
24/// computes the layout of the workspace required to compute a matrix's
25/// bidiagonalization
26pub fn bidiag_in_place_scratch<T: ComplexField>(
27	nrows: usize,
28	ncols: usize,
29	par: ParShape,
30	params: Spec<BidiagParams, T>,
31) -> StackReq {
32	_ = par;
33	_ = params;
34	let tail = StackReq::all_of(&[
35		temp_mat_scratch::<T>(nrows, 1),
36		temp_mat_scratch::<T>(ncols, 1),
37		temp_mat_scratch::<T>(ncols, 1),
38		temp_mat_scratch::<T>(ncols, 1),
39	]);
40	if Ord::min(nrows, ncols) > BLOCK_FLOOR {
41		// the correction matrices live through a panel and its trailing update, and are dead
42		// before the tail allocates its own pair, so the two phases share the stack
43		let blocked = StackReq::all_of(&[
44			temp_mat_scratch::<T>(nrows, BLOCK_SIZE),
45			temp_mat_scratch::<T>(ncols, BLOCK_SIZE),
46			temp_mat_scratch::<T>(BLOCK_SIZE, 2),
47			temp_mat_scratch::<T>(ncols, 1),
48		]);
49		StackReq::any_of(&[blocked, tail])
50	} else {
51		tail
52	}
53}
54/// bidiagonalization tuning parameters.
55#[derive(Debug, Copy, Clone)]
56pub struct BidiagParams {
57	/// floor of the parallel regions: [`ParThreshold::Derived`] (the default) runs a step in
58	/// parallel when its serial time pays for its two sections; [`ParThreshold::Fixed`] counts
59	/// elements of the trailing block `(m-k-1)*(n-k-1)`
60	pub par_threshold: ParThreshold,
61	#[doc(hidden)]
62	pub non_exhaustive: NonExhaustive,
63}
64impl<T: ComplexField> Auto<T> for BidiagParams {
65	fn auto() -> Self {
66		Self {
67			par_threshold: ParThreshold::Derived,
68			non_exhaustive: NonExhaustive(()),
69		}
70	}
71}
72/// computes a matrix $A$'s bidiagonalization such that $A = U B V^H$
73///
74/// $B$ is a bidiagonal matrix stored in $A$'s diagonal and superdiagonal
75///
76/// $U$ is a sequence of householder reflections stored in the unit lower
77/// triangular half of $A$, with the householder coefficients being stored in
78/// `H_left`
79///
80/// $V$ is a sequence of householder reflections stored in the unit upper
81/// triangular half of $A$ (excluding the diagonal), with the householder
82/// coefficients being stored in `H_right`
83pub fn bidiag_in_place<T: ComplexField>(
84	A: MatMut<'_, T>,
85	H_left: MatMut<'_, T>,
86	H_right: MatMut<'_, T>,
87	par: Par,
88	stack: &mut MemStack,
89	params: Spec<BidiagParams, T>,
90) {
91	let params = params.config;
92	let m = A.nrows();
93	let n = A.ncols();
94	let size = Ord::min(m, n);
95	let bl = H_left.nrows();
96	let br = H_right.nrows();
97	assert!(H_left.ncols() == size);
98	assert!(H_right.ncols() == size.saturating_sub(1));
99	let mut A = A;
100	let mut Hl = H_left;
101	let mut Hr = H_right;
102	let mut par = par;
103	// `Never` keeps the whole call sequential, the compact-factor upgrades included
104	if matches!(params.par_threshold, ParThreshold::Never) {
105		par = Par::Seq;
106	}
107	// blocked phase: whole panels while the remaining dimension clears the floor. its regions are
108	// two vector products per step and one update per panel, and they run fastest when the caller
109	// holds a team around the call (see the crate docs): spinning members that keep their column
110	// range of the trailing block. the library takes no reservation itself; measured at n = 512,
111	// 8T: 19.2 ms with the team held against 25.5 ms with members woken per region.
112	let mut p = 0;
113	if size > BLOCK_FLOOR {
114		p = bidiag_blocked_phase(
115			A.rb_mut(),
116			Hl.rb_mut().row_mut(0),
117			Hr.rb_mut().row_mut(0),
118			par.reborrow(),
119			params.par_threshold,
120			stack,
121		);
122	}
123
124	// unblocked tail: the trailing block is already current, so it starts with fresh local
125	// indexing and no pending deferred pair
126	bidiag_unblocked(
127		A.rb_mut().get_mut(p.., p..),
128		Hl.rb_mut().row_mut(0).get_mut(p..),
129		Hr.rb_mut().row_mut(0).get_mut(p..),
130		par.reborrow(),
131		stack,
132		params.par_threshold,
133		p,
134	);
135
136	let mut j = 0;
137	while j < size {
138		let bl = Ord::min(bl, size - j);
139		let mut Hl = Hl.rb_mut().get_mut(..bl, j..j + bl);
140		for k in 0..bl {
141			Hl[(k, k)] = Hl[(0, k)].copy();
142		}
143		upgrade_householder_factor(
144			Hl.rb_mut(),
145			A.rb().get(j.., j..j + bl),
146			bl,
147			1,
148			par.reborrow(),
149		);
150		j += bl;
151	}
152	if size > 0 {
153		let size = size - 1;
154		let A = A.rb().get(..size, 1..);
155		let mut Hr = Hr.rb_mut().get_mut(.., ..size);
156		let mut j = 0;
157		while j < size {
158			let br = Ord::min(br, size - j);
159			let mut Hr = Hr.rb_mut().get_mut(..br, j..j + br);
160			for k in 0..br {
161				Hr[(k, k)] = Hr[(0, k)].copy();
162			}
163			upgrade_householder_factor(
164				Hr.rb_mut(),
165				A.transpose().get(j.., j..j + br),
166				br,
167				1,
168				par.reborrow(),
169			);
170			j += br;
171		}
172	}
173}
174
175/// the blocked phase: whole panels of `BLOCK_SIZE` while the remaining dimension exceeds
176/// `BLOCK_FLOOR`. returns the offset at which the unblocked tail starts.
177fn bidiag_blocked_phase<T: ComplexField>(
178	A: MatMut<'_, T>,
179	Hl: RowMut<'_, T>,
180	Hr: RowMut<'_, T>,
181	mut par: Par,
182	par_threshold: ParThreshold,
183	stack: &mut MemStack,
184) -> usize {
185	let mut A = A;
186	let mut Hl = Hl;
187	let mut Hr = Hr;
188	let m = A.nrows();
189	let n = A.ncols();
190	let size = Ord::min(m, n);
191	let (mut X, stack) =
192		unsafe { temp_mat_uninit::<T, _, _>(m, BLOCK_SIZE, stack) };
193	let (mut Y, stack) =
194		unsafe { temp_mat_uninit::<T, _, _>(n, BLOCK_SIZE, stack) };
195	let (mut de, stack) =
196		unsafe { temp_mat_uninit::<T, _, _>(BLOCK_SIZE, 2, stack) };
197	let (mut w, _) = unsafe { temp_mat_uninit::<T, _, _>(n, 1, stack) };
198	let mut X = X.as_mat_mut();
199	let mut Y = Y.as_mat_mut();
200	let mut de = de.as_mat_mut();
201	let mut w = w.as_mat_mut();
202	let mut p = 0;
203	while size - p > BLOCK_FLOOR {
204		// the decision is made on the full remaining area, never latched by a panel. It is the
205		// floor of the panel only: the panel's own products choose their widths, so a paying
206		// panel keeps the whole `par` rather than capping them at this estimate's width
207		let width = par_threshold.region_width::<T>(&par, (m - p) * (n - p), rate::bidiag(), 2);
208		let width = if width > 1 { par.degree() } else { 1 };
209		with_width(par.reborrow(), width, |par| {
210			bidiag_blocked_panel(
211				A.rb_mut().get_mut(p.., p..),
212				Hl.rb_mut().get_mut(p..p + BLOCK_SIZE),
213				Hr.rb_mut().get_mut(p..p + BLOCK_SIZE),
214				X.rb_mut().get_mut(p.., ..),
215				Y.rb_mut().get_mut(p.., ..),
216				de.rb_mut(),
217				w.rb_mut().col_mut(0).get_mut(p..),
218				par,
219			);
220		});
221		p += BLOCK_SIZE;
222	}
223	p
224}
225
226/// reduces the first `ib` columns and rows of `A` (`ib = X.ncols()`), then updates the trailing
227/// block `A[ib.., ib..]` once with two level-3 products.
228///
229/// conventions, the ones the rest of this file uses: the left reflector of step `i` is the column
230/// `u_i = A[i.., i]` with unit head and `H_i = I - u_i u_i^H / tl_i`; the right one is the row
231/// `r_i = A[i, i + 1..]` with unit head and `G_i = I - r_i^H r_i / tr_i`. after `ib` steps
232///
233/// `A_new = A - V Y^H - X R`
234///
235/// where `V = [u_0 .. u_ib-1]`, `Y[.., i] = A_i^H u_i / tl_i` (`A_i` is the matrix after `i`
236/// full steps), `R = [r_0; ..; r_ib-1]` and `X[.., i] = A_i' r_i^H / tr_i` (`A_i'` is `A_i` after
237/// the left reflection of step `i`). `Y[.., i]` is meaningful at rows `> i`, `X[.., i]` at rows
238/// `> i`; the rows at or above the diagonal hold the recurrence's short temporaries, as in
239/// LAPACK's `dlabrd`. the reflector heads stay `1` in `A` during the panel and the update, and
240/// the diagonal and superdiagonal they replace are kept in `de` and written back at the end.
241///
242/// the row of each step is gathered into the contiguous `w` (length `n`) for its update, its
243/// reflector and the products it is the vector of, and scattered back once: a row of `A` has a
244/// stride of `m` elements, and both the vector products and the reflector norm want a contiguous
245/// operand.
246fn bidiag_blocked_panel<T: ComplexField>(
247	A: MatMut<'_, T>,
248	Hl: RowMut<'_, T>,
249	Hr: RowMut<'_, T>,
250	X: MatMut<'_, T>,
251	Y: MatMut<'_, T>,
252	de: MatMut<'_, T>,
253	w: ColMut<'_, T>,
254	mut par: Par,
255) {
256	let mut A = A;
257	let mut w = w;
258	let mut Hl = Hl;
259	let mut Hr = Hr;
260	let mut X = X;
261	let mut Y = Y;
262	let mut de = de;
263	let m = A.nrows();
264	let n = A.ncols();
265	let ib = X.ncols();
266	assert!(all(
267		Y.ncols() == ib,
268		X.nrows() == m,
269		Y.nrows() == n,
270		Ord::min(m, n) > ib,
271		Hl.ncols() == ib,
272		Hr.ncols() == ib,
273	));
274
275	for i in 0..ib {
276		// 1. column i, rows i.., against the corrections of the steps before it:
277		//    A[i.., i] -= V[i.., ..i] Y[i, ..i]^H + X[i.., ..i] R[..i, i]
278		let tl = {
279			let (left, right) = A.rb_mut().split_at_col_mut(i);
280			let (top, mut col) = right.col_mut(0).split_at_row_mut(i);
281			if i > 0 {
282				matmul(
283					col.rb_mut(),
284					Accum::Add,
285					left.rb().get(i.., ..),
286					Y.rb().row(i).get(..i).adjoint(),
287					-one::<T>(),
288					Par::Seq,
289				);
290				matmul(
291					col.rb_mut(),
292					Accum::Add,
293					X.rb().get(i.., ..i),
294					top.rb(),
295					-one::<T>(),
296					Par::Seq,
297				);
298			}
299			// 2. left reflector
300			let (mut head, tail) = col.split_at_row_mut(1);
301			let head = &mut head[0];
302			let HouseholderInfo { tau: tl, .. } =
303				make_householder_in_place(head, tail);
304			de[(i, 0)] = head.copy();
305			*head = one();
306			Hl[i] = tl.to_cplx();
307			tl
308		};
309		let ref tl_inv = tl.recip();
310
311		// 3. Y[i+1.., i] = A_i[i.., i+1..]^H u_i / tl_i, with A_i expressed through the stored
312		//    A and the corrections:
313		//    A[i.., i+1..]^H u - Y[i+1.., ..i] (V[i.., ..i]^H u) - R[..i, i+1..]^H (X[i.., ..i]^H u)
314	#[cfg(feature = "rayon")]
315		{
316			let u = A.rb().col(i).get(i..);
317			let (y_prev, y_i) = Y.rb_mut().split_at_col_mut(i);
318			let (mut t, mut y) = y_i.col_mut(0).split_at_row_mut(i + 1);
319			matmul(
320				y.rb_mut(),
321				Accum::Replace,
322				A.rb().get(i.., i + 1..).adjoint(),
323				u,
324				one(),
325				par.reborrow(),
326			);
327			if i > 0 {
328				let mut t = t.rb_mut().get_mut(..i);
329				matmul(
330					t.rb_mut(),
331					Accum::Replace,
332					A.rb().get(i.., ..i).adjoint(),
333					u,
334					one(),
335					Par::Seq,
336				);
337				matmul(
338					y.rb_mut(),
339					Accum::Add,
340					y_prev.rb().get(i + 1.., ..),
341					t.rb(),
342					-one::<T>(),
343					Par::Seq,
344				);
345				matmul(
346					t.rb_mut(),
347					Accum::Replace,
348					X.rb().get(i.., ..i).adjoint(),
349					u,
350					one(),
351					Par::Seq,
352				);
353				matmul(
354					y.rb_mut(),
355					Accum::Add,
356					A.rb().get(..i, i + 1..).adjoint(),
357					t.rb(),
358					-one::<T>(),
359					Par::Seq,
360				);
361			}
362			z!(y.rb_mut()).for_each(|uz!(y)| *y = y.mul_real(tl_inv));
363		}
364
365		// 4. row i, columns i+1.., after the left reflection of this step and the corrections
366		//    of the steps before it, computed on the contiguous gather `w = A[i, i+1..]^T`:
367		//    w -= conj(Y[i+1.., ..i+1]) V[i, ..i+1]^T + R[..i, i+1..]^T X[i, ..i]^T
368		let tr = {
369			let (top, bot) = A.rb_mut().split_at_row_mut(i);
370			let (left, mut row) = bot.row_mut(0).split_at_col_mut(i + 1);
371			let mut w = w.rb_mut().get_mut(i + 1..);
372			w.copy_from(row.rb().transpose());
373			matmul(
374				w.rb_mut(),
375				Accum::Add,
376				Y.rb().get(i + 1.., ..i + 1).conjugate(),
377				left.rb().transpose(),
378				-one::<T>(),
379				Par::Seq,
380			);
381			if i > 0 {
382				matmul(
383					w.rb_mut(),
384					Accum::Add,
385					top.rb().get(.., i + 1..).transpose(),
386					X.rb().row(i).get(..i).transpose(),
387					-one::<T>(),
388					Par::Seq,
389				);
390			}
391			// 5. right reflector, on the gather, then scattered back into the row
392			let (mut head, tail) = w.rb_mut().split_at_row_mut(1);
393			let head = &mut head[0];
394			let HouseholderInfo { tau: tr, .. } =
395				make_householder_in_place(head, tail);
396			de[(i, 1)] = head.copy();
397			*head = one();
398			Hr[i] = tr.to_cplx();
399			row.rb_mut().transpose_mut().copy_from(w.rb());
400			tr
401		};
402		let ref tr_inv = tr.recip();
403
404		// 6. X[i+1.., i] = A_i'[i+1.., i+1..] r_i^H / tr_i, with `r^H = conj(w)`:
405		//    A[i+1.., i+1..] r^H - V[i+1.., ..i+1] (Y[i+1.., ..i+1]^H r^H) - X[i+1.., ..i] (R[..i, i+1..] r^H)
406		{
407			let r = w.rb().get(i + 1..).conjugate();
408			let (x_prev, x_i) = X.rb_mut().split_at_col_mut(i);
409			let (mut t, mut x) = x_i.col_mut(0).split_at_row_mut(i + 1);
410			matmul(
411				x.rb_mut(),
412				Accum::Replace,
413				A.rb().get(i + 1.., i + 1..),
414				r,
415				one(),
416				par.reborrow(),
417			);
418			matmul(
419				t.rb_mut(),
420				Accum::Replace,
421				Y.rb().get(i + 1.., ..i + 1).adjoint(),
422				r,
423				one(),
424				Par::Seq,
425			);
426			matmul(
427				x.rb_mut(),
428				Accum::Add,
429				A.rb().get(i + 1.., ..i + 1),
430				t.rb(),
431				-one::<T>(),
432				Par::Seq,
433			);
434			if i > 0 {
435				let mut t = t.rb_mut().get_mut(..i);
436				matmul(
437					t.rb_mut(),
438					Accum::Replace,
439					A.rb().get(..i, i + 1..),
440					r,
441					one(),
442					Par::Seq,
443				);
444				matmul(
445					x.rb_mut(),
446					Accum::Add,
447					x_prev.rb().get(i + 1.., ..),
448					t.rb(),
449					-one::<T>(),
450					Par::Seq,
451				);
452			}
453			z!(x.rb_mut()).for_each(|uz!(x)| *x = x.mul_real(tr_inv));
454		}
455	}
456
457	// trailing update, once per panel: A[ib.., ib..] -= V[ib.., ..] Y[ib.., ..]^H + X[ib.., ..] R[.., ib..]
458	{
459		let (top, bot) = A.rb_mut().split_at_row_mut(ib);
460		let (v, mut a22) = bot.split_at_col_mut(ib);
461		matmul(
462			a22.rb_mut(),
463			Accum::Add,
464			v.rb(),
465			Y.rb().get(ib.., ..).adjoint(),
466			-one::<T>(),
467			par.reborrow(),
468		);
469		matmul(
470			a22.rb_mut(),
471			Accum::Add,
472			X.rb().get(ib.., ..),
473			top.rb().get(.., ib..),
474			-one::<T>(),
475			par.reborrow(),
476		);
477	}
478	// the heads were 1 for the products; put the bidiagonal back
479	for i in 0..ib {
480		A[(i, i)] = de[(i, 0)].copy();
481		A[(i, i + 1)] = de[(i, 1)].copy();
482	}
483}
484
485/// the unblocked bidiagonalization of `A`, one reflector pair per step with the trailing update
486/// deferred by one step and fused with the next step's product. `row_offset` is `A`'s row
487/// position in the matrix it is a tail of, used only as a SIMD alignment hint.
488///
489/// the row of each step lives in a contiguous buffer while it is worked on: a row of `A` has a
490/// stride of `m`, and the deferred update, the norm, the scaling, the right reflector and the
491/// inner product each walk it once. `w` holds the current row (columns `k + 1..`), `wp` the
492/// previous step's row reflector, which the deferred update and the fused product read; the row
493/// is gathered once at the start of the step and scattered back once at its end.
494fn bidiag_unblocked<T: ComplexField>(
495	A: MatMut<'_, T>,
496	Hl: RowMut<'_, T>,
497	Hr: RowMut<'_, T>,
498	par: Par,
499	stack: &mut MemStack,
500	par_threshold: ParThreshold,
501	row_offset: usize,
502) {
503	let m = A.nrows();
504	let n = A.ncols();
505	let size = Ord::min(m, n);
506	alloca!('stack: {
507		let mut y = unsafe { row![uninit::<T>, n] };
508		let mut z = unsafe { col![uninit::<T>, m] };
509		let mut w0 = unsafe { row![uninit::<T>, n] };
510		let mut w1 = unsafe { row![uninit::<T>, n] };
511	});
512	let mut A = A;
513	let mut Hl = Hl;
514	let mut Hr = Hr;
515	let mut par = par;
516	// `Never` keeps the whole call sequential (the steps decide their own width otherwise)
517	if matches!(par_threshold, ParThreshold::Never) {
518		par = Par::Seq;
519	}
520	let mut w = w0.rb_mut();
521	let mut wp = w1.rb_mut();
522	for k in 0..size {
523		let mut A = A.rb_mut();
524		let (_, _, A10, A11) = A.rb_mut().split_at_mut(k, k);
525		let (A10, A20) = A10.split_first_row_mut().unwrap();
526		let (mut A11, A12, A21, mut A22) = A11.split_at_mut(1, 1);
527		let mut A12 = A12.row_mut(0);
528		let mut A21 = A21.col_mut(0);
529		let a11 = &mut A11[(0, 0)];
530		let (y1, mut y2) = y.rb_mut().split_at_col_mut(k).1.split_at_col_mut(1);
531		let (z1, mut z2) = z.rb_mut().split_at_row_mut(k).1.split_at_row_mut(1);
532		// the row of this step, contiguous, and the previous step's row reflector
533		let mut wk = w.rb_mut().split_at_col_mut(k + 1).1;
534		wk.copy_from(A12.rb());
535		let vp = wp.rb().split_at_col(k + 1).1;
536		if k > 0 {
537			let ref y1 = y1[0].copy();
538			let ref z1 = z1[0].copy();
539			let k1 = k - 1;
540			let ref up0 = A10[k1];
541			let up = A20.rb().col(k1);
542			*a11 -= up0 * y1 + z1;
543			z!(A21.rb_mut(), up.rb(), z2.rb()).for_each(
544				|uz!(a, u, z): Zip!(&mut T, &T, &T)| *a -= u * y1 + z,
545			);
546			z!(wk.rb_mut(), y2.rb(), vp).for_each(
547				|uz!(a, y, v): Zip!(&mut T, &T, &T)| {
548					*a -= up0 * y + z1 * v;
549				},
550			);
551		}
552		let HouseholderInfo { tau: tl, .. } =
553			make_householder_in_place(a11, A21.rb_mut());
554		let ref tl_inv = tl.recip();
555		Hl[k] = tl.to_cplx();
556		// the step's two products run at the widest width that pays, decided per step
557		let width = par_threshold.region_width::<T>(&par, (m - k - 1) * (n - k - 1), rate::bidiag(), 2);
558		if k > 0 {
559			let k1 = k - 1;
560			let up = A20.rb().col(k1);
561			with_width(par.reborrow(), width, |mut par| {
562				match par.reborrow() {
563					Par::Seq => bidiag_fused_op(
564						A22.rb_mut(),
565						A21.rb(),
566						up.rb(),
567						z2.rb(),
568						y2.rb_mut(),
569						vp,
570						simd_align(row_offset + k + 1),
571					),
572		#[cfg(feature = "rayon")]
573					_ => {
574						use rayon::prelude::*;
575						let nthreads =
576							core::num::NonZeroUsize::new(par.degree()).unwrap().get();
577						crate::utils::thread::for_each(
578							par.reborrow(),
579							nthreads,
580							A22.rb_mut()
581								.par_col_partition_mut(nthreads)
582								.zip_eq(y2.rb_mut().par_partition_mut(nthreads))
583								.zip_eq(vp.par_partition(nthreads)),
584							|((A22, y2), vp)| {
585								bidiag_fused_op(
586									A22,
587									A21.rb(),
588									up.rb(),
589									z2.rb(),
590									y2,
591									vp.rb(),
592									simd_align(row_offset + k + 1),
593								);
594							},
595						);
596					},
597				}
598			});
599		} else {
600			with_width(par.reborrow(), width, |par| {
601				matmul(
602					y2.rb_mut(),
603					Accum::Replace,
604					A21.rb().adjoint(),
605					A22.rb(),
606					one(),
607					par,
608				);
609			});
610		}
611		z!(y2.rb_mut(), wk.rb_mut()).for_each(|uz!(y, a)| {
612			*y = (&*y + &*a).mul_real(tl_inv);
613			*a -= &*y;
614		});
615		// a subnormal row norm has an infinite reciprocal, so rescale the row first: reference
616		// `dlarfg` does the same when its normaliser underflows (main's fix, carried onto the
617		// contiguous row). the row's own norm is kept for the head's restoration below.
618		let norm = wk.rb().norm_l2();
619		if norm != zero() {
620			let ref norm_inv = if norm >= min_positive() {
621				(&norm).recip()
622			} else {
623				let ref shift = from_f64::<T::Real>(4503599627370496.0);
624				z!(wk.rb_mut()).for_each(|uz!(a)| *a = a.mul_real(shift));
625				(&norm * shift).recip()
626			};
627			z!(wk.rb_mut()).for_each(|uz!(a)| *a = a.mul_real(norm_inv));
628		}
629		with_width(par.reborrow(), width, |par| {
630			matmul(
631				z2.rb_mut(),
632				Accum::Replace,
633				A22.rb(),
634				wk.rb().adjoint(),
635				one(),
636				par,
637			);
638		});
639		if k + 1 == size {
640			A12.copy_from(wk.rb());
641			break;
642		}
643		let (mut wk_a, mut wk_b) = wk.rb_mut().split_at_col_mut(1);
644		let A22_a = A22.rb().col(0);
645		let (y2_a, y2_b) = y2.rb().split_at_col(1);
646		let y2_a = &y2_a[0];
647		let a12_a = &mut wk_a[0];
648		let HouseholderInfo {
649			tau: tr,
650			head_with_beta_inv: m,
651			..
652		} = make_householder_in_place(a12_a, wk_b.rb_mut().transpose_mut());
653		let ref tr_inv = tr.recip();
654		Hr[k] = tr.to_cplx();
655		let beta = a12_a.copy();
656		*a12_a = a12_a.mul_real(norm);
657		let ref b = y2_a
658			+ dot::inner_prod(y2_b, Conj::No, wk_b.rb().transpose(), Conj::Yes);
659		if m != infinity() {
660			z!(z2.rb_mut(), A21.rb(), A22_a.rb()).for_each(
661				|uz!(z, u, a): Zip!(&mut T, &T, &T)| {
662					let w = &*z - a * beta.conj();
663					let w = w * m.conj();
664					let w = w - u * b;
665					*z = w.mul_real(tr_inv);
666				},
667			);
668		} else {
669			z!(z2.rb_mut(), A21.rb(), A22_a.rb()).for_each(
670				|uz!(z, u, a): Zip!(&mut T, &T, &T)| {
671					let w = a - u * b;
672					*z = w.mul_real(tr_inv);
673				},
674			);
675		}
676		// the row goes back into `A`, where it is the stored right reflector; the next step reads
677		// it from `wp`
678		A12.copy_from(wk.rb());
679		core::mem::swap(&mut w, &mut wp);
680	}
681}
682fn bidiag_fused_op<T: ComplexField>(
683	A22: MatMut<'_, T>,
684	u: ColRef<'_, T>,
685	up: ColRef<'_, T>,
686	z: ColRef<'_, T>,
687	y: RowMut<'_, T>,
688	vp: RowRef<'_, T>,
689	align: usize,
690) {
691	let mut A22 = A22;
692	if const { T::SIMD_CAPABILITIES.is_simd() } {
693		if let (Some(A22), Some(u), Some(up), Some(z)) = (
694			A22.rb_mut().try_as_col_major_mut(),
695			u.try_as_col_major(),
696			up.try_as_col_major(),
697			z.try_as_col_major(),
698		) {
699			bidiag_fused_op_simd(A22, u, up, z, y, vp, align);
700		} else {
701			bidiag_fused_op_fallback(A22, u, up, z, y, vp);
702		}
703	} else {
704		bidiag_fused_op_fallback(A22, u, up, z, y, vp);
705	}
706}
707fn bidiag_fused_op_fallback<T: ComplexField>(
708	A22: MatMut<'_, T>,
709	u: ColRef<'_, T>,
710	up: ColRef<'_, T>,
711	z: ColRef<'_, T>,
712	y: RowMut<'_, T>,
713	vp: RowRef<'_, T>,
714) {
715	let mut A22 = A22;
716	let mut y = y;
717	matmul(A22.rb_mut(), Accum::Add, up, y.rb(), -one::<T>(), Par::Seq);
718	matmul(A22.rb_mut(), Accum::Add, z, vp, -one::<T>(), Par::Seq);
719	matmul(
720		y.rb_mut(),
721		Accum::Replace,
722		u.adjoint(),
723		A22.rb(),
724		one(),
725		Par::Seq,
726	);
727}
728fn bidiag_fused_op_simd<'M, 'N, T: ComplexField>(
729	A22: MatMut<'_, T, usize, usize, ContiguousFwd>,
730	u: ColRef<'_, T, usize, ContiguousFwd>,
731	up: ColRef<'_, T, usize, ContiguousFwd>,
732	z: ColRef<'_, T, usize, ContiguousFwd>,
733	y: RowMut<'_, T, usize>,
734	vp: RowRef<'_, T, usize>,
735	align: usize,
736) {
737	struct Impl<'a, 'M, 'N, T: ComplexField> {
738		A22: MatMut<'a, T, Dim<'M>, Dim<'N>, ContiguousFwd>,
739		u: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
740		up: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
741		z: ColRef<'a, T, Dim<'M>, ContiguousFwd>,
742		y: RowMut<'a, T, Dim<'N>>,
743		vp: RowRef<'a, T, Dim<'N>>,
744		align: usize,
745	}
746	impl<'a, 'M, 'N, T: ComplexField> pulp::WithSimd for Impl<'a, 'M, 'N, T> {
747		type Output = ();
748
749		#[inline(always)]
750		fn with_simd<S: pulp::Simd>(self, simd: S) -> Self::Output {
751			let Self {
752				mut A22,
753				u,
754				up,
755				z,
756				mut y,
757				vp,
758				align,
759			} = self;
760			let m = A22.nrows();
761			let n = A22.ncols();
762			let simd = SimdCtx::<T, S>::new_align(T::simd_ctx(simd), m, align);
763			let indices = simd.batch_indices::<4>();
764			for j in n.indices() {
765				let mut A = A22.rb_mut().col_mut(j);
766				let mut acc = [simd.zero(); 4];
767				let yj = simd.splat(-&y[j]);
768				let vj = simd.splat(-&vp[j]);
769				simd_iter!(for (IDX, i) in [indices; 4] {
770					let mut a = simd.read(A.rb(), i);
771					a = simd.mul_add(simd.read(up, i), yj, a);
772					a = simd.mul_add(simd.read(z, i), vj, a);
773					simd.write(A.rb_mut(), i, a);
774					acc[IDX] = simd.conj_mul_add(simd.read(u, i), a, acc[IDX]);
775				});
776				let acc0 = simd.add(acc[0], acc[1]);
777				let acc2 = simd.add(acc[2], acc[3]);
778				let acc0 = simd.add(acc0, acc2);
779				y[j] = simd.reduce_sum(acc0);
780			}
781		}
782	}
783	with_dim!(M, A22.nrows());
784	with_dim!(N, A22.ncols());
785	dispatch!(
786		Impl {
787			A22: A22.as_shape_mut(M, N),
788			u: u.as_row_shape(M),
789			up: up.as_row_shape(M),
790			z: z.as_row_shape(M),
791			y: y.as_col_shape_mut(N),
792			vp: vp.as_col_shape(N),
793			align,
794		},
795		Impl,
796		T
797	)
798}
799#[cfg(test)]
800mod tests {
801	use super::*;
802	use crate::stats::prelude::*;
803	use crate::utils::approx::*;
804	use crate::{Mat, assert, c64};
805	use dyn_stack::MemBuffer;
806
807	/// bidiagonalizes `A` and rebuilds it from the factors
808	fn reconstruct<T: ComplexField>(
809		A: &Mat<T>,
810		mut par: Par,
811		params: BidiagParams,
812	) -> (Mat<T>, Mat<T>) {
813		let m = A.nrows();
814		let n = A.ncols();
815		let size = Ord::min(m, n);
816		let bl = 4;
817		let br = 3;
818		let mut Hl = Mat::zeros(bl, size);
819		let mut Hr = Mat::zeros(br, size - 1);
820		let mut UV = A.clone();
821		let mut mem =
822			MemBuffer::new(bidiag_in_place_scratch::<T>(m, n, par.shape(), params.into()));
823		bidiag_in_place(
824			UV.rb_mut(),
825			Hl.rb_mut(),
826			Hr.rb_mut(),
827			par.reborrow(),
828			MemStack::new(&mut mem),
829			params.into(),
830		);
831		let mut B = A.clone();
832		let mut B = B.as_mut();
833		apply_block_householder_sequence_transpose_on_the_left_in_place_with_conj(
834			UV.rb().get(.., ..size),
835			Hl.rb(),
836			Conj::Yes,
837			B.rb_mut(),
838			par.reborrow(),
839			MemStack::new(&mut MemBuffer::new(
840				apply_block_householder_sequence_transpose_on_the_left_in_place_scratch::<T>(
841					n - 1, 1, m,
842				),
843			)),
844		);
845		let V = UV.rb().get(..size - 1, 1..size);
846		let B1 = B.rb_mut().get_mut(.., 1..size);
847		apply_block_householder_sequence_on_the_right_in_place_with_conj(
848			V.transpose(),
849			Hr.rb(),
850			Conj::Yes,
851			B1,
852			par.reborrow(),
853			MemStack::new(&mut MemBuffer::new(
854				apply_block_householder_sequence_on_the_right_in_place_scratch::<
855					T,
856				>(n - 1, 1, m),
857			)),
858		);
859		for j in 0..n {
860			for i in 0..m {
861				if i > j || j > i + 1 {
862					UV[(i, j)] = zero();
863				}
864			}
865		}
866		(UV, B.to_owned())
867	}
868
869	/// the shapes the tests cover: the two historical small ones (unblocked), and three that
870	/// cross at least two blocked panels before the unblocked tail
871	const SHAPES: [(usize, usize); 5] =
872		[(8, 4), (8, 8), (300, 300), (400, 300), (200, 200)];
873
874	/// The sequential budget, then the threaded one on a team of four.
875	fn for_each_budget(mut f: impl FnMut(Par<'_>, BidiagParams)) {
876		f(Par::Seq, <BidiagParams as Auto<f64>>::auto());
877		// the default floor (`ParThreshold::Derived`) leaves a small trailing area sequential, so
878		// the threaded case pins it to zero to exercise the threaded panel update and tail
879		let mut threaded = <BidiagParams as Auto<f64>>::auto();
880		threaded.par_threshold = ParThreshold::Fixed(0);
881		crate::team::with_team(4, |par| f(par, threaded));
882	}
883
884	#[test]
885	fn test_bidiag_real() {
886		let rng = &mut StdRng::seed_from_u64(0);
887		for (m, n) in SHAPES {
888			let A = CwiseMatDistribution {
889				nrows: m,
890				ncols: n,
891				dist: StandardNormal,
892			}
893			.rand::<Mat<f64>>(rng);
894			for_each_budget(|par, params| {
895				let (UV, B) = reconstruct(&A, par, params);
896				let approx_eq =
897					CwiseMat(ApproxEq::<f64>::eps() * ((m * n) as f64).sqrt());
898				assert!(UV ~ B);
899			});
900		}
901	}
902	/// The parallel route must produce the same factorization as the sequential one, for the
903	/// complex scalars too: `A = Hl B Hr^H` up to the tolerance of the reflectors. The floor is
904	/// forced (`Fixed(1)`) so every step takes the team path, at sizes that split into fewer
905	/// columns than the team has members.
906	#[test]
907	fn bidiag_on_a_team_reconstructs() {
908		fn check<T: crate::ComplexField<Real = f64>>(a: Mat<T>, bl: usize, br: usize) {
909			let (m, n) = a.shape();
910			let size = Ord::min(m, n);
911			let mut params: BidiagParams = crate::Auto::<T>::auto();
912			params.par_threshold = crate::utils::cost::ParThreshold::Fixed(1);
913			if size < 2 {
914				return;
915			}
916			let mut hl = Mat::<T>::zeros(bl, size);
917			let mut hr = Mat::<T>::zeros(br, size - 1);
918			let mut uv = a.clone();
919			crate::team::with_team(4, |par| {
920				bidiag_in_place(
921					uv.rb_mut(),
922					hl.rb_mut(),
923					hr.rb_mut(),
924					par,
925					MemStack::new(&mut MemBuffer::new(bidiag_in_place_scratch::<T>(
926						m,
927						n,
928						ParShape::team(4),
929						params.into(),
930					))),
931					params.into(),
932				);
933			});
934			let mut rebuilt = a.clone();
935			let mut rebuilt = rebuilt.as_mut();
936			apply_block_householder_sequence_transpose_on_the_left_in_place_with_conj(
937				uv.rb().get(.., ..size),
938				hl.rb(),
939				Conj::Yes,
940				rebuilt.rb_mut(),
941				Par::Seq,
942				MemStack::new(&mut MemBuffer::new(
943					apply_block_householder_sequence_transpose_on_the_left_in_place_scratch::<T>(n - 1, bl, m),
944				)),
945			);
946			let v = uv.rb().get(..size - 1, 1..size);
947			let a1 = rebuilt.rb_mut().get_mut(.., 1..size);
948			apply_block_householder_sequence_on_the_right_in_place_with_conj(
949				v.transpose(),
950				hr.as_ref(),
951				Conj::Yes,
952				a1,
953				Par::Seq,
954				MemStack::new(&mut MemBuffer::new(
955					apply_block_householder_sequence_on_the_right_in_place_scratch::<T>(n - 1, br, m),
956				)),
957			);
958			for j in 0..n {
959				for i in 0..m {
960					if i > j || j > i + 1 {
961						uv[(i, j)] = T::zero();
962					}
963				}
964			}
965			let mut err = 0.0f64;
966			let mut nonfinite = false;
967			for j in 0..n {
968				for i in 0..m {
969					let d = (uv[(i, j)].clone() - rebuilt[(i, j)].clone()).abs();
970					nonfinite |= !d.is_finite() || !uv[(i, j)].abs().is_finite();
971					if d.is_finite() {
972						err = err.max(d);
973					}
974				}
975			}
976			core::assert!(!nonfinite, "{m}x{n}: a non-finite factor or difference");
977			let scale = (0..n)
978				.flat_map(|j| (0..m).map(move |i| (i, j)))
979				.fold(0.0f64, |m, (i, j)| m.max(a[(i, j)].abs()));
980			core::assert!(err <= 1e-10 * scale.max(1.0), "{m}x{n}: {err} {scale}");
981		}
982		let rng = &mut StdRng::seed_from_u64(11);
983		// the fixture of the cost tests: a real-valued matrix in the complex dtype, where the
984		// parallel route takes a different but valid factorization from the sequential one
985		let real_as_c64 = Mat::<c64>::from_fn(96, 96, |i, j| {
986			let h = (i * 2654435761usize).wrapping_add(j * 40503).wrapping_add(977) % 1009;
987			c64::new(h as f64 / 504.0 - 1.0, 0.0)
988		});
989		check::<c64>(real_as_c64, 8, 8);
990		for (bl, br) in [(4usize, 3usize), (8, 8)] {
991			for (m, n) in [(8usize, 4usize), (8, 8), (40, 40), (96, 96), (37, 13)] {
992				check::<f64>(Mat::from_fn(m, n, |_, _| rand::Rng::random::<f64>(rng)), bl, br);
993				check::<c64>(
994					Mat::from_fn(m, n, |_, _| {
995						c64::new(
996							rand::Rng::random::<f64>(rng),
997							rand::Rng::random::<f64>(rng),
998						)
999					}),
1000					bl,
1001					br,
1002				);
1003			}
1004		}
1005	}
1006	#[test]
1007	fn test_bidiag_cplx() {
1008		let rng = &mut StdRng::seed_from_u64(0);
1009		for (m, n) in SHAPES {
1010			let A = CwiseMatDistribution {
1011				nrows: m,
1012				ncols: n,
1013				dist: ComplexDistribution::new(StandardNormal, StandardNormal),
1014			}
1015			.rand::<Mat<c64>>(rng);
1016			for_each_budget(|par, params| {
1017				let (UV, B) = reconstruct(&A, par, params);
1018				let approx_eq =
1019					CwiseMat(ApproxEq::<f64>::eps() * ((m * n) as f64).sqrt());
1020				assert!(UV ~ B);
1021			});
1022		}
1023	}
1024}