Skip to main content

faer_lu/partial_pivoting/
compute.rs

1use dyn_stack::{PodStack, SizeOverflow, StackReq};
2use faer_core::{
3    assert, debug_assert,
4    group_helpers::*,
5    mul::matmul,
6    permutation::{Index, PermutationMut, SignedIndex},
7    solve::solve_unit_lower_triangular_in_place,
8    unzipped, zipped, ComplexField, Entity, MatMut, Parallelism, SimdCtx,
9};
10use faer_entity::*;
11use reborrow::*;
12
13#[inline(always)]
14fn swap_two_elems<E: ComplexField>(mut m: MatMut<'_, E>, i: usize, j: usize) {
15    debug_assert!(m.ncols() == 1);
16    debug_assert!(i < m.nrows());
17    debug_assert!(j < m.nrows());
18    unsafe {
19        let a = m.read_unchecked(i, 0);
20        let b = m.read_unchecked(j, 0);
21        m.write_unchecked(i, 0, b);
22        m.write_unchecked(j, 0, a);
23    }
24}
25
26#[inline(always)]
27fn swap_two_elems_contiguous<E: ComplexField>(m: MatMut<'_, E>, i: usize, j: usize) {
28    debug_assert!(m.ncols() == 1);
29    debug_assert!(m.row_stride() == 1);
30    debug_assert!(i < m.nrows());
31    debug_assert!(j < m.nrows());
32    unsafe {
33        let ptr = m.as_ptr_mut();
34        let ptr_a = E::faer_map(
35            E::faer_copy(&ptr),
36            #[inline(always)]
37            |ptr| ptr.add(i),
38        );
39        let ptr_b = E::faer_map(
40            E::faer_copy(&ptr),
41            #[inline(always)]
42            |ptr| ptr.add(j),
43        );
44
45        E::faer_map(
46            E::faer_zip(ptr_a, ptr_b),
47            #[inline(always)]
48            |(a, b)| core::ptr::swap(a, b),
49        );
50    }
51}
52
53#[inline(never)]
54fn lu_in_place_unblocked<E: ComplexField, I: Index>(
55    mut matrix: MatMut<'_, E>,
56    col_start: usize,
57    n: usize,
58    transpositions: &mut [I],
59) -> usize {
60    let m = matrix.nrows();
61    let ncols = matrix.ncols();
62    assert!(m >= n);
63
64    let truncate = <I::Signed as SignedIndex>::truncate;
65
66    if n == 0 {
67        return 0;
68    }
69
70    let mut n_transpositions = 0;
71
72    let arch = E::Simd::default();
73
74    for (k, t) in transpositions.iter_mut().enumerate() {
75        let mut imax = 0;
76        {
77            let col = k + col_start;
78            let col = matrix.rb().col(col).subrows(k, m - k);
79            let m = col.nrows();
80
81            let mut max = E::Real::faer_zero();
82
83            for i in 0..m {
84                let abs = unsafe { col.read_unchecked(i) }.faer_score();
85                if abs > max {
86                    imax = i;
87                    max = abs;
88                }
89            }
90
91            imax += k;
92        }
93
94        *t = I::from_signed(truncate(imax - k));
95
96        if imax != k {
97            n_transpositions += 1;
98        }
99
100        if k != imax {
101            for j in 0..ncols {
102                unsafe {
103                    let mk = matrix.read_unchecked(k, j);
104                    let mi = matrix.read_unchecked(imax, j);
105                    matrix.write_unchecked(k, j, mi);
106                    matrix.write_unchecked(imax, j, mk);
107                }
108            }
109        }
110
111        let (_, _, _, middle_right) = matrix.rb_mut().split_at_mut(0, col_start);
112        let (_, _, middle, _) = middle_right.split_at_mut(0, n);
113        update(arch, middle, k);
114    }
115
116    n_transpositions
117}
118
119struct Update<'a, E: ComplexField> {
120    matrix: MatMut<'a, E>,
121    j: usize,
122}
123
124impl<E: ComplexField> pulp::WithSimd for Update<'_, E> {
125    type Output = ();
126
127    #[inline(always)]
128    fn with_simd<S: pulp::Simd>(self, simd: S) -> Self::Output {
129        let Self { mut matrix, j } = self;
130
131        debug_assert_eq!(matrix.row_stride(), 1);
132
133        let m = matrix.nrows();
134        let inv = matrix.read(j, j).faer_inv();
135        for i in j + 1..m {
136            unsafe {
137                matrix.write_unchecked(i, j, matrix.read_unchecked(i, j).faer_mul(inv));
138            }
139        }
140        let (_, top_right, bottom_left, bottom_right) = matrix.rb_mut().split_at_mut(j + 1, j + 1);
141        let lhs = bottom_left.rb().col(j);
142        let rhs = top_right.rb().row(j);
143        let mut mat = bottom_right;
144
145        let lhs = SliceGroup::<'_, E>::new(lhs.try_get_contiguous_col());
146
147        let simd = SimdFor::<E, S>::new(simd);
148        let offset = simd.align_offset(lhs);
149        let (lhs_head, lhs_body, lhs_tail) = simd.as_aligned_simd(lhs, offset);
150
151        for k in 0..mat.ncols() {
152            let acc = SliceGroupMut::<'_, E>::new(mat.rb_mut().try_get_contiguous_col_mut(k));
153            let rhs = simd.splat(rhs.read(k).faer_neg());
154            let (acc_head, acc_body, acc_tail) = simd.as_aligned_simd_mut(acc, offset);
155
156            #[inline(always)]
157            fn process<E: ComplexField, S: pulp::Simd>(
158                simd: SimdFor<E, S>,
159                mut acc: impl Write<Output = SimdGroupFor<E, S>>,
160                lhs: impl Read<Output = SimdGroupFor<E, S>>,
161                rhs: SimdGroupFor<E, S>,
162            ) {
163                let zero = simd.splat(E::faer_zero());
164                acc.write(simd.mul_add_e(rhs, lhs.read_or(zero), acc.read_or(zero)));
165            }
166
167            process(simd, acc_head, lhs_head, rhs);
168            for (acc, lhs) in acc_body.into_mut_iter().zip(lhs_body.into_ref_iter()) {
169                process(simd, acc, lhs, rhs);
170            }
171            process(simd, acc_tail, lhs_tail, rhs);
172        }
173    }
174}
175
176fn update<E: ComplexField>(arch: E::Simd, mut matrix: MatMut<E>, j: usize) {
177    if matrix.row_stride() == 1 {
178        arch.dispatch(Update { matrix, j });
179    } else {
180        let m = matrix.nrows();
181        let inv = matrix.read(j, j).faer_inv();
182        for i in j + 1..m {
183            matrix.write(i, j, matrix.read(i, j).faer_mul(inv));
184        }
185        let (_, top_right, bottom_left, bottom_right) = matrix.rb_mut().split_at_mut(j + 1, j + 1);
186        let lhs = bottom_left.rb().col(j);
187        let rhs = top_right.rb().row(j);
188        let mut mat = bottom_right;
189
190        for k in 0..mat.ncols() {
191            let col = mat.rb_mut().col_mut(k);
192            let rhs = rhs.read(k);
193            zipped!(col.as_2d_mut(), lhs.as_2d()).for_each(|unzipped!(mut x, lhs)| {
194                x.write(x.read().faer_sub(lhs.read().faer_mul(rhs)))
195            });
196        }
197    }
198}
199
200#[allow(clippy::extra_unused_type_parameters)]
201fn recursion_threshold<E: Entity>(_m: usize) -> usize {
202    16
203}
204
205#[inline]
206#[allow(clippy::extra_unused_type_parameters)]
207// we want remainder to be a multiple of register size
208fn blocksize<E: Entity>(n: usize) -> usize {
209    let base_rem = n / 2;
210    n - if n >= 32 {
211        (base_rem + 15) / 16 * 16
212    } else if n >= 16 {
213        (base_rem + 7) / 8 * 8
214    } else if n >= 8 {
215        (base_rem + 3) / 4 * 4
216    } else {
217        base_rem
218    }
219}
220
221#[doc(hidden)]
222pub fn lu_in_place_impl<I: Index, E: ComplexField>(
223    mut matrix: MatMut<'_, E>,
224    col_start: usize,
225    n: usize,
226    transpositions: &mut [I],
227    parallelism: Parallelism,
228) -> usize {
229    let m = matrix.nrows();
230    let full_n = matrix.ncols();
231
232    debug_assert!(m >= n);
233
234    if n <= recursion_threshold::<E>(m) {
235        return lu_in_place_unblocked(matrix, col_start, n, transpositions);
236    }
237
238    // recursing is fine-ish since we halve the blocksize at each recursion step
239    let bs = blocksize::<E>(n);
240
241    let mut n_transpositions = 0;
242
243    n_transpositions += lu_in_place_impl(
244        matrix.rb_mut().submatrix_mut(0, col_start, m, n),
245        0,
246        bs,
247        &mut transpositions[..bs],
248        parallelism,
249    );
250
251    let (mat_top_left, mut mat_top_right, mat_bot_left, mut mat_bot_right) = matrix
252        .rb_mut()
253        .submatrix_mut(0, col_start, m, n)
254        .split_at_mut(bs, bs);
255
256    solve_unit_lower_triangular_in_place(mat_top_left.rb(), mat_top_right.rb_mut(), parallelism);
257    matmul(
258        mat_bot_right.rb_mut(),
259        mat_bot_left.rb(),
260        mat_top_right.rb(),
261        Some(E::faer_one()),
262        E::faer_one().faer_neg(),
263        parallelism,
264    );
265
266    n_transpositions += lu_in_place_impl(
267        matrix.rb_mut().submatrix_mut(bs, col_start, m - bs, n),
268        bs,
269        n - bs,
270        &mut transpositions[bs..],
271        parallelism,
272    );
273
274    let parallelism = if m * (full_n - n) > 128 * 128 {
275        parallelism
276    } else {
277        Parallelism::None
278    };
279
280    if matrix.row_stride() == 1 {
281        faer_core::for_each_raw(
282            col_start + (full_n - (col_start + n)),
283            |j| {
284                let j = if j >= col_start { col_start + n + j } else { j };
285                let mut col = unsafe { matrix.rb().col(j).const_cast() };
286                for (i, &t) in transpositions[..bs].iter().enumerate() {
287                    swap_two_elems_contiguous(
288                        col.rb_mut().as_2d_mut(),
289                        i,
290                        t.to_signed().zx() + i.to_signed().zx(),
291                    );
292                }
293                let (_, mut col) = col.split_at_mut(bs);
294                for (i, &t) in transpositions[bs..].iter().enumerate() {
295                    swap_two_elems_contiguous(
296                        col.rb_mut().as_2d_mut(),
297                        i,
298                        t.to_signed().zx() + i.to_signed().zx(),
299                    );
300                }
301            },
302            parallelism,
303        );
304    } else {
305        faer_core::for_each_raw(
306            col_start + (full_n - (col_start + n)),
307            |j| {
308                let j = if j >= col_start { col_start + n + j } else { j };
309                let mut col = unsafe { matrix.rb().col(j).const_cast() };
310                for (i, &t) in transpositions[..bs].iter().enumerate() {
311                    swap_two_elems(
312                        col.rb_mut().as_2d_mut(),
313                        i,
314                        t.to_signed().zx() + i.to_signed().zx(),
315                    );
316                }
317                let (_, mut col) = col.split_at_mut(bs);
318                for (i, &t) in transpositions[bs..].iter().enumerate() {
319                    swap_two_elems(
320                        col.rb_mut().as_2d_mut(),
321                        i,
322                        t.to_signed().zx() + i.to_signed().zx(),
323                    );
324                }
325            },
326            parallelism,
327        );
328    }
329
330    n_transpositions
331}
332
333#[derive(Default, Copy, Clone)]
334#[non_exhaustive]
335pub struct PartialPivLuComputeParams {}
336
337#[derive(Copy, Clone, Debug)]
338pub struct PartialPivLuInfo {
339    pub transposition_count: usize,
340}
341
342/// Computes the size and alignment of required workspace for performing an LU
343/// decomposition with partial pivoting.
344pub fn lu_in_place_req<I: Index, E: Entity>(
345    m: usize,
346    n: usize,
347    parallelism: Parallelism,
348    params: PartialPivLuComputeParams,
349) -> Result<StackReq, SizeOverflow> {
350    let _ = &params;
351    let _ = &parallelism;
352
353    let size = Ord::min(n, m);
354    StackReq::try_new::<I>(size)
355}
356
357/// Computes the LU decomposition of the given matrix with partial pivoting, replacing the matrix
358/// with its factors in place.
359///
360/// The decomposition is such that:
361/// $$PA = LU,$$
362/// where $P$ is a permutation matrix, $L$ is a unit lower triangular matrix, and $U$ is an upper
363/// triangular matrix.
364///
365/// $L$ is stored in the strictly lower triangular half of `matrix`, with an implicit unit
366/// diagonal, $U$ is stored in the upper triangular half of `matrix`, and the permutation
367/// representing $P$, as well as its inverse, are stored in `perm` and `perm_inv` respectively.
368///
369/// After the function returns, `perm` contains the order of the rows after pivoting, i.e. the
370/// result is the same as computing the non-pivoted LU decomposition of the matrix `matrix[perm,
371/// :]`. `perm_inv` contains its inverse permutation.
372///
373/// # Output
374///
375/// - The number of transpositions that constitute the permutation,
376/// - a structure representing the permutation $P$.
377///
378/// # Panics
379///
380/// - Panics if the length of the permutation slices is not equal to the number of rows of the
381/// matrix.
382/// - Panics if the provided memory in `stack` is insufficient (see [`lu_in_place_req`]).
383pub fn lu_in_place<'out, I: Index, E: ComplexField>(
384    matrix: MatMut<'_, E>,
385    perm: &'out mut [I],
386    perm_inv: &'out mut [I],
387    parallelism: Parallelism,
388    stack: PodStack<'_>,
389    params: PartialPivLuComputeParams,
390) -> (PartialPivLuInfo, PermutationMut<'out, I, E>) {
391    let _ = &params;
392    let truncate = <I::Signed as SignedIndex>::truncate;
393
394    assert!(perm.len() == matrix.nrows());
395    assert!(perm_inv.len() == matrix.nrows());
396
397    #[cfg(feature = "perf-warn")]
398    if (matrix.col_stride().unsigned_abs() == 1 || matrix.row_stride().unsigned_abs() != 1)
399        && faer_core::__perf_warn!(LU_WARN)
400    {
401        log::warn!(target: "faer_perf", "LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides.");
402    }
403
404    let mut matrix = matrix;
405    let mut stack = stack;
406    let m = matrix.nrows();
407    let n = matrix.ncols();
408    let size = Ord::min(n, m);
409
410    for (i, p) in perm.iter_mut().enumerate() {
411        *p = I::from_signed(truncate(i));
412    }
413
414    let (transpositions, _) = stack
415        .rb_mut()
416        .make_with(size, |_| I::from_signed(truncate(0)));
417    let n_transpositions = lu_in_place_impl(matrix.rb_mut(), 0, size, transpositions, parallelism);
418
419    for (idx, t) in transpositions.iter().enumerate() {
420        perm.swap(idx, idx + t.to_signed().zx());
421    }
422
423    let (_, _, left, right) = matrix.split_at_mut(0, size);
424
425    if m < n {
426        solve_unit_lower_triangular_in_place(left.rb(), right, parallelism);
427    }
428
429    for (i, &p) in perm.iter().enumerate() {
430        perm_inv[p.to_signed().zx()] = I::from_signed(truncate(i));
431    }
432
433    (
434        PartialPivLuInfo {
435            transposition_count: n_transpositions,
436        },
437        unsafe { PermutationMut::new_unchecked(perm, perm_inv) },
438    )
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::partial_pivoting::reconstruct;
445    use assert_approx_eq::assert_approx_eq;
446    use dyn_stack::GlobalPodBuffer;
447    use faer_core::{assert, permutation::PermutationRef, Mat, MatRef};
448    use rand::random;
449
450    macro_rules! make_stack {
451        ($req: expr) => {
452            ::dyn_stack::PodStack::new(&mut ::dyn_stack::GlobalPodBuffer::new($req.unwrap()))
453        };
454    }
455
456    fn reconstruct_matrix<I: Index, E: ComplexField>(
457        lu_factors: MatRef<'_, E>,
458        row_perm: PermutationRef<'_, I, E>,
459    ) -> Mat<E> {
460        let m = lu_factors.nrows();
461        let n = lu_factors.ncols();
462        let mut dst = Mat::zeros(m, n);
463        reconstruct::reconstruct(
464            dst.as_mut(),
465            lu_factors,
466            row_perm,
467            Parallelism::Rayon(0),
468            make_stack!(reconstruct::reconstruct_req::<I, E>(
469                m,
470                n,
471                Parallelism::Rayon(0)
472            )),
473        );
474        dst
475    }
476
477    #[test]
478    fn compute_lu() {
479        for (m, n) in [
480            (10, 10),
481            (4, 4),
482            (2, 4),
483            (2, 20),
484            (2, 2),
485            (20, 20),
486            (4, 2),
487            (20, 2),
488            (40, 20),
489            (20, 40),
490            (40, 60),
491            (60, 40),
492            (200, 100),
493            (100, 200),
494            (200, 200),
495        ] {
496            let mut mat = Mat::from_fn(m, n, |_, _| random::<f64>());
497            let mat_orig = mat.clone();
498            let mut perm = vec![0usize; m];
499            let mut perm_inv = vec![0; m];
500
501            let mut mem = GlobalPodBuffer::new(
502                lu_in_place_req::<usize, f64>(m, n, Parallelism::Rayon(8), Default::default())
503                    .unwrap(),
504            );
505            let mut stack = PodStack::new(&mut mem);
506
507            let (_, row_perm) = lu_in_place(
508                mat.as_mut(),
509                &mut perm,
510                &mut perm_inv,
511                Parallelism::Rayon(8),
512                stack.rb_mut(),
513                Default::default(),
514            );
515            let reconstructed = reconstruct_matrix(mat.as_ref(), row_perm.rb());
516
517            for i in 0..m {
518                for j in 0..n {
519                    assert_approx_eq!(mat_orig.read(i, j), reconstructed.read(i, j));
520                }
521            }
522        }
523    }
524
525    #[test]
526    fn compute_lu_non_contiguous() {
527        for (m, n) in [
528            (10, 10),
529            (4, 4),
530            (2, 4),
531            (2, 20),
532            (2, 2),
533            (20, 20),
534            (4, 2),
535            (20, 2),
536            (40, 20),
537            (20, 40),
538            (40, 60),
539            (60, 40),
540            (200, 100),
541            (100, 200),
542            (200, 200),
543        ] {
544            let mut mat = Mat::from_fn(m, n, |_, _| random::<f64>());
545            let mut mat = mat.as_mut().reverse_rows_mut();
546            let mat_orig = mat.to_owned();
547            let mut perm = vec![0usize; m];
548            let mut perm_inv = vec![0; m];
549
550            let mut mem = GlobalPodBuffer::new(
551                lu_in_place_req::<usize, f64>(m, n, Parallelism::Rayon(8), Default::default())
552                    .unwrap(),
553            );
554            let mut stack = PodStack::new(&mut mem);
555
556            let (_, row_perm) = lu_in_place(
557                mat.rb_mut(),
558                &mut perm,
559                &mut perm_inv,
560                Parallelism::Rayon(8),
561                stack.rb_mut(),
562                Default::default(),
563            );
564            let reconstructed = reconstruct_matrix(mat.rb(), row_perm.rb());
565
566            for i in 0..m {
567                for j in 0..n {
568                    assert_approx_eq!(mat_orig.read(i, j), reconstructed.read(i, j));
569                }
570            }
571        }
572    }
573
574    #[test]
575    fn compute_lu_row_major() {
576        for (m, n) in [
577            (3, 3),
578            (2, 2),
579            (4, 2),
580            (2, 4),
581            (4, 4),
582            (10, 10),
583            (2, 20),
584            (20, 20),
585            (20, 2),
586            (40, 20),
587            (20, 40),
588            (40, 60),
589            (60, 40),
590            (200, 100),
591            (100, 200),
592            (200, 200),
593        ] {
594            let mut mat = Mat::from_fn(n, m, |_, _| random::<f64>());
595            let mut mat = mat.as_mut().transpose_mut();
596            let mat_orig = mat.to_owned();
597            let mut perm = vec![0usize; m];
598            let mut perm_inv = vec![0; m];
599
600            let mut mem = GlobalPodBuffer::new(
601                lu_in_place_req::<usize, f64>(m, n, Parallelism::Rayon(8), Default::default())
602                    .unwrap(),
603            );
604            let mut stack = PodStack::new(&mut mem);
605
606            let (_, row_perm) = lu_in_place(
607                mat.rb_mut(),
608                &mut perm,
609                &mut perm_inv,
610                Parallelism::Rayon(8),
611                stack.rb_mut(),
612                Default::default(),
613            );
614            let reconstructed = reconstruct_matrix(mat.rb(), row_perm.rb());
615
616            for i in 0..m {
617                for j in 0..n {
618                    assert_approx_eq!(mat_orig.read(i, j), reconstructed.read(i, j));
619                }
620            }
621        }
622    }
623}