Skip to main content

a/math/matrix/
cholesky.rs

1//! The Cholesky decomposition of a Hermitian positive definite matrix.
2//!
3//! Up to 7 × 7, or 4 × 4 for complex elements, the decomposition is unrolled ([`tiny`]); from
4//! 8 to 9, or 10 for `f64`, it is the dot products of [`dot`]. Up to 32 × 32, or 16 × 16 for
5//! complex elements, a leaf factors it column by column ([`narrow`]): for `f64` and `f32` the
6//! NEON leaf (`aarch64`) holds each column in vector registers while the columns before it are
7//! subtracted, its pivot summed apart in scalars so that the square root need not wait for it to
8//! be taken out of a vector; complex elements take [`dot`]. Larger matrices are halved: `L11`, then
9//! `L21 = A21·L11⁻*` by a triangular solve on the right, reading a copy of `L11` in working memory
10//! taken once for the whole recursion, then `A22 - L21·L21*` on its lower triangle, in blocks of
11//! [`UPDATE`] columns. The upper triangle of `L` is zeroed as its parts are finished.
12//!
13//! # Speed
14//!
15//! Measured on an Apple M4 Max, one core, `f64`, in µs, the median of 21 interleaved batches,
16//! against faer 0.24 (`llt`, `Par::Seq`) and nalgebra 0.35 (`cholesky`):
17//!
18//! | n | ours | faer | nalgebra |
19//! | --- | --- | --- | --- |
20//! | 2 | 0.018 | 0.071 | 0.019 |
21//! | 4 | 0.026 | 0.114 | 0.039 |
22//! | 8 | 0.104 | 0.211 | 0.124 |
23//! | 16 | 0.256 | 0.436 | 0.399 |
24//! | 32 | 1.09 | 1.42 | 1.99 |
25//! | 64 | 5.37 | 7.86 | 10.9 |
26//! | 128 | 31.7 | 33.1 | 57.1 |
27//! | 256 | 170 | 187 | 432 |
28//! | 512 | 1 197 | 1 403 | 3 735 |
29//! | 1024 | 7 778 | 8 753 | 29 377 |
30//!
31//! Apple's Accelerate, on the matrix unit (SME), ran 2 to 6 times faster from 32 × 32 up
32//! (`dpotrf` 220 µs at 512).
33
34use super::blas::{Op, Side, Uplo, gemm, trsm, with_scratch};
35use super::{Matrix, Scalar};
36use crate::math::complex::Float;
37
38// The leaves of `f64` and `f32` in NEON intrinsics on little-endian aarch64; the portable leaf
39// serves the other types and targets, and Miri.
40#[cfg(all(target_arch = "aarch64", target_endian = "little", not(miri)))]
41mod aarch64;
42
43/// The Cholesky decomposition `A = L·L*` of a Hermitian (for real elements, symmetric) positive
44/// definite matrix, `L` lower triangular with a positive real diagonal. From
45/// [`Matrix::cholesky`].
46///
47/// # Examples
48///
49/// ```
50/// use a::math::matrix::Matrix;
51///
52/// let a = Matrix::from_rows(&[[4.0, 2.0], [2.0, 5.0]]);
53/// let c = a.cholesky().unwrap();
54/// assert_eq!(c.l(), Matrix::from_rows(&[[2.0, 0.0], [1.0, 2.0]]));
55/// assert_eq!(c.det(), 16.0);
56/// assert!(Matrix::from_rows(&[[1.0, 2.0], [2.0, 1.0]]).cholesky().is_none());
57/// ```
58#[derive(Clone, Debug)]
59pub struct Cholesky<T> {
60    /// `L`, zeros above the diagonal.
61    l: Matrix<T>,
62}
63
64impl<T: Scalar> Matrix<T> {
65    /// The Cholesky decomposition `A = L·L*`, or `None` when the matrix is not positive
66    /// definite: a pivot is not positive, or not a number. Only the lower triangle is read, the
67    /// upper one taken to be its conjugate. About `n³/3` operations, nearly all of them in
68    /// products of blocks.
69    ///
70    /// # Panics
71    ///
72    /// Panics if the matrix is not square.
73    #[track_caller]
74    pub fn cholesky(&self) -> Option<Cholesky<T>> {
75        assert!(self.is_square(), "the matrix is not square");
76        let n = self.rows();
77        let mut l = self.clone();
78        match n {
79            0 => {}
80            1 => tiny::<T, 1>(&mut l.data)?,
81            2 => tiny::<T, 2>(&mut l.data)?,
82            3 => tiny::<T, 3>(&mut l.data)?,
83            4 => tiny::<T, 4>(&mut l.data)?,
84            // Unrolled past 4, complex elements ran slower than by the loops; real ones ran
85            // fastest unrolled up to 7, by the dot products of [`dot`] at 8 and 9 (and 10 for
86            // `f64`), and by the NEON leaves past that.
87            5 if !T::IS_COMPLEX => tiny::<T, 5>(&mut l.data)?,
88            6 if !T::IS_COMPLEX => tiny::<T, 6>(&mut l.data)?,
89            7 if !T::IS_COMPLEX => tiny::<T, 7>(&mut l.data)?,
90            8 | 9 => dot(&mut l.data, n, n)?,
91            10 if size_of::<T>() == 8 => dot(&mut l.data, n, n)?,
92            _ if n <= narrow_order::<T>() => narrow(&mut l.data, n, n)?,
93            _ => {
94                // The working memory of the recursion: a copy of the largest `L11`.
95                let n1 = split::<T>(n);
96                with_scratch(n1 * n1, |work| factor(&mut l.data, n, n, work))?;
97            }
98        }
99        Some(Cholesky { l })
100    }
101}
102
103impl<T: Scalar> Cholesky<T> {
104    /// `L`, lower triangular with a positive real diagonal.
105    pub fn l(&self) -> Matrix<T> {
106        self.l.clone()
107    }
108
109    /// The determinant of the matrix, the square of the product of the diagonal of `L`: real
110    /// and positive.
111    pub fn det(&self) -> T::Real {
112        let mut d = T::Real::ONE;
113        for x in self.l.diagonal() {
114            let r = x.re();
115            d *= r * r;
116        }
117        d
118    }
119
120    /// The solution `X` of `A·X = B`.
121    ///
122    /// # Panics
123    ///
124    /// Panics if `B` has not as many rows as the matrix.
125    #[track_caller]
126    pub fn solve(&self, b: &Matrix<T>) -> Matrix<T> {
127        let n = self.l.rows();
128        assert_eq!(b.rows(), n, "the right-hand side has not as many rows");
129        let mut x = b.clone();
130        let cols = x.cols();
131        trsm(
132            Side::Left,
133            Uplo::Lower,
134            Op::N,
135            false,
136            n,
137            cols,
138            &self.l.data,
139            n,
140            &mut x.data,
141            n,
142        );
143        trsm(
144            Side::Left,
145            Uplo::Lower,
146            Op::C,
147            false,
148            n,
149            cols,
150            &self.l.data,
151            n,
152            &mut x.data,
153            n,
154        );
155        x
156    }
157
158    /// The inverse of the matrix.
159    pub fn inverse(&self) -> Matrix<T> {
160        self.solve(&Matrix::identity(self.l.rows()))
161    }
162}
163
164/// Blocks up to this order are factored without recursion: 32 for real elements, whose leaves
165/// keep their columns in vector registers, and 16 for complex ones.
166const fn narrow_order<T: Scalar>() -> usize {
167    if T::IS_COMPLEX { 16 } else { 32 }
168}
169
170/// Where the recursion splits `n` columns: the first part a multiple of [`narrow_order`].
171fn split<T: Scalar>(n: usize) -> usize {
172    (n / 2).next_multiple_of(narrow_order::<T>()).min(n - 1)
173}
174
175/// Factors the `n × n` block at the start of `a` in place, its lower triangle: `L11`, then
176/// `L21 = A21·L11⁻*`, then `A22 - L21·L21*` the same way, and zeroes the upper triangle.
177/// `L11` and `L21` share their columns, so the solve reads a copy of `L11` in `work`, of at
178/// least `split(n)²` elements.
179fn factor<T: Scalar>(a: &mut [T], n: usize, ld: usize, work: &mut [T]) -> Option<()> {
180    if n <= narrow_order::<T>() {
181        return narrow(a, n, ld);
182    }
183    let n1 = split::<T>(n);
184    let n2 = n - n1;
185    factor(a, n1, ld, work)?;
186    let (left, right) = a.split_at_mut(n1 * ld);
187    let l11 = &mut work[..n1 * n1];
188    for j in 0..n1 {
189        l11[j * n1 + j..(j + 1) * n1].copy_from_slice(&left[j * ld + j..j * ld + n1]);
190    }
191    trsm(
192        Side::Right,
193        Uplo::Lower,
194        Op::C,
195        false,
196        n2,
197        n1,
198        l11,
199        n1,
200        &mut left[n1..],
201        ld,
202    );
203    // `A22 -= L21·L21*` on the lower triangle only, a block of columns at a time from its
204    // diagonal down: about half the products of the whole block.
205    let l21 = &left[n1..];
206    let a22 = &mut right[n1..];
207    let mut j0 = 0;
208    while j0 < n2 {
209        let w = UPDATE.min(n2 - j0);
210        gemm(
211            n2 - j0,
212            w,
213            n1,
214            -T::one(),
215            &l21[j0..],
216            ld,
217            Op::N,
218            &l21[j0..],
219            ld,
220            Op::C,
221            T::one(),
222            &mut a22[j0 + j0 * ld..],
223            ld,
224        );
225        j0 += w;
226    }
227    factor(a22, n2, ld, work)?;
228    // The upper triangle of `L` is zeros: `A12`, and the triangles of the leaves.
229    for j in 0..n2 {
230        right[j * ld..j * ld + n1].fill(T::zero());
231    }
232    Some(())
233}
234
235/// The width of the column blocks of the update of the trailing block.
236const UPDATE: usize = 64;
237
238/// Factors a small block column by column, then zeroes the upper triangle: by the NEON leaves
239/// for `f64` and `f32`, and by [`dot`] for the other types and targets, and Miri.
240fn narrow<T: Scalar>(a: &mut [T], n: usize, ld: usize) -> Option<()> {
241    #[cfg(all(target_arch = "aarch64", target_endian = "little", not(miri)))]
242    if let Some(done) = narrow_neon(a, n, ld) {
243        return done.then_some(());
244    }
245    dot(a, n, ld)
246}
247
248/// [`narrow`] without the NEON leaves: each element of column `j` is a dot product of its row
249/// with row `j`, the products not fused, so that the sums' chains wait on additions alone.
250fn dot<T: Scalar>(a: &mut [T], n: usize, ld: usize) -> Option<()> {
251    for j in 0..n {
252        let mut d = a[j + j * ld].re();
253        for p in 0..j {
254            d -= a[j + p * ld].abs2();
255        }
256        if d.is_nan() || d <= T::Real::ZERO {
257            return None;
258        }
259        let d = d.sqrt();
260        a[j + j * ld] = T::from_real(d);
261        let r = T::Real::ONE / d;
262        for i in j + 1..n {
263            let mut s = a[i + j * ld];
264            for p in 0..j {
265                s -= a[i + p * ld] * a[j + p * ld].conj();
266            }
267            a[i + j * ld] = s.scale(r);
268        }
269    }
270    for j in 1..n {
271        a[j * ld..j * ld + j].fill(T::zero());
272    }
273    Some(())
274}
275
276/// [`narrow`] by the NEON leaves, for `f64` and `f32`: whether the block was positive
277/// definite, or `None` for the other types.
278#[cfg(all(target_arch = "aarch64", target_endian = "little", not(miri)))]
279fn narrow_neon<T: Scalar>(a: &mut [T], n: usize, ld: usize) -> Option<bool> {
280    use core::any::TypeId;
281    let is_f64 = TypeId::of::<T>() == TypeId::of::<f64>();
282    if !(is_f64 || TypeId::of::<T>() == TypeId::of::<f32>()) {
283        return None;
284    }
285    assert!((1..=aarch64::ROWS).contains(&n) && (n - 1) * ld + n <= a.len());
286    let len = a.len();
287    let a = a.as_mut_ptr();
288    // SAFETY: `T` is the type each branch casts to, and the block lies in the slice, as
289    // checked.
290    Some(unsafe {
291        if is_f64 {
292            let a = a.cast::<f64>();
293            match n {
294                1..=2 => aarch64::leaf_f64::<1, 2>(a, n, ld, len),
295                3..=4 => aarch64::leaf_f64::<2, 4>(a, n, ld, len),
296                5..=6 => aarch64::leaf_f64::<3, 6>(a, n, ld, len),
297                7..=8 => aarch64::leaf_f64::<4, 8>(a, n, ld, len),
298                9..=12 => aarch64::leaf_f64::<6, 12>(a, n, ld, len),
299                13..=16 => aarch64::leaf_f64::<8, 16>(a, n, ld, len),
300                17..=24 => aarch64::leaf_f64::<12, 24>(a, n, ld, len),
301                _ => aarch64::leaf_f64::<16, 32>(a, n, ld, len),
302            }
303        } else {
304            let a = a.cast::<f32>();
305            match n {
306                1..=4 => aarch64::leaf_f32::<1, 4>(a, n, ld, len),
307                5..=8 => aarch64::leaf_f32::<2, 8>(a, n, ld, len),
308                9..=12 => aarch64::leaf_f32::<3, 12>(a, n, ld, len),
309                13..=16 => aarch64::leaf_f32::<4, 16>(a, n, ld, len),
310                17..=24 => aarch64::leaf_f32::<6, 24>(a, n, ld, len),
311                _ => aarch64::leaf_f32::<8, 32>(a, n, ld, len),
312            }
313        }
314    })
315}
316
317/// [`narrow`] of an `N × N` matrix, `N` known, so that its loops unroll.
318#[inline(always)]
319fn tiny<T: Scalar, const N: usize>(a: &mut [T]) -> Option<()> {
320    let a = &mut a[..N * N];
321    for j in 0..N {
322        for p in 0..j {
323            let s = a[j + p * N].conj();
324            for i in j..N {
325                a[i + j * N] = a[i + j * N].mul_sub(a[i + p * N], s);
326            }
327        }
328        let d = pivot(&mut a[j + j * N..(j + 1) * N])?;
329        let r = T::Real::ONE / d;
330        for i in j + 1..N {
331            a[i + j * N] = a[i + j * N].scale(r);
332        }
333    }
334    for j in 1..N {
335        for i in 0..j {
336            a[i + j * N] = T::zero();
337        }
338    }
339    Some(())
340}
341
342/// The pivot of a column, its first element, updated: its square root, written in its place,
343/// or `None` when it is not positive, or not a number. Its imaginary part, the rounding of
344/// products of conjugates, is dropped.
345#[inline(always)]
346fn pivot<T: Scalar>(col: &mut [T]) -> Option<T::Real> {
347    let d = col[0].re();
348    if d.is_nan() || d <= T::Real::ZERO {
349        return None;
350    }
351    let d = d.sqrt();
352    col[0] = T::from_real(d);
353    Some(d)
354}