Files
OneCAD-Simulation/crates/solver/src/cholesky.rs
T

1204 lines
48 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Sparse Cholesky factorization A = L Lᵀ of a symmetric positive definite matrix: supernodal,
//! left-looking, multithreaded, with a fixed operation order.
//!
//! **Symbolic.** The elimination tree (J. W. H. Liu, *The role of elimination trees in sparse
//! factorization*, SIAM J. Matrix Anal. Appl. 11(1):134–172, 1990) and the column counts of L
//! come from the row subtrees (T. A. Davis, *Direct Methods for Sparse Linear Systems*, SIAM 2006,
//! §4.1–4.4), so the factor's size is checked against a cap before any memory is committed. The
//! columns are renumbered in a postorder of the tree (an equivalent ordering: the same fill,
//! Liu 1990), so every subtree is a contiguous range of columns, and consecutive columns with
//! nested structures are merged into **fundamental supernodes** (J. W. H. Liu, E. G. Ng, B. W.
//! Peyton, *On finding supernodes for sparse matrix computations*, SIAM J. Matrix Anal. Appl.
//! 14(1):242–252, 1993): a supernode's columns share one row structure and are stored as one
//! dense lower trapezoid, so the factor holds exactly the entries of L.
//!
//! **Numeric** (left-looking supernodal Cholesky: E. Ng, B. W. Peyton, *Block sparse Cholesky
//! algorithms on advanced uniprocessor computers*, SIAM J. Sci. Comput. 14(5):1034–1056, 1993;
//! E. Rothberg, A. Gupta, *An efficient block-oriented approach to parallel sparse Cholesky
//! factorization*, SIAM J. Sci. Comput. 15(6):1413–1439, 1994): a supernode starts as its
//! columns of A, receives the update L_K L_Kᵀ of every descendant supernode K whose rows reach
//! it (K in increasing order), and is then factored densely by panels of [`PANEL`] columns: a
//! panel is updated with the supernode's columns before it, its diagonal block is factored column
//! by column, and the rows below are solved against that block.
//!
//! **Threads and determinism.** Each entry of L is computed by one task, by a sequence of
//! operations fixed by the matrix and the constants [`TILE`] and [`PANEL`]: sums over a
//! supernode's columns run in column order from zero, and updates are subtracted in supernode
//! order. Disjoint subtrees of the supernodal tree are factored concurrently (a supernode reads
//! only its descendants), and the dense work of a large supernode is split into row tiles;
//! neither changes any entry's sequence of operations, so the factor is the same bit for bit on
//! any number of threads and on every platform (multiplications and additions are never fused).
//!
//! A pivot at or below `PIVOT_TOLERANCE` times the matrix's own diagonal entry means the matrix
//! is singular to working precision (an unconstrained rigid-body motion, a disconnected part, a
//! zero-stiffness direction); the factorization stops and names the column: the first such column
//! in the postorder, whichever thread meets it.
use fieldcraft_parallel as parallel;
use crate::{Error, Result};
/// Relative pivot size below which the matrix counts as singular.
pub const PIVOT_TOLERANCE: f64 = 1e-10;
/// Rows per tile of the dense kernels (a tile's partial sums stay in registers and L1).
pub const TILE: usize = 64;
/// Columns per panel of a supernode's dense factorization.
pub const PANEL: usize = 64;
/// Dense work (multiply–adds) from which a kernel's tiles run in parallel.
const PARALLEL_WORK: usize = 1 << 21;
/// Subtrees are handed to threads in pieces of about this fraction of the total work.
const SUBTREE_SHARE: usize = 256;
/// The upper triangle of a symmetric matrix by columns: column k holds A(i, k) for i ≤ k, rows
/// sorted.
#[derive(Debug, Clone)]
pub struct UpperColumns {
pub n: usize,
pub ptr: Vec<usize>,
pub idx: Vec<usize>,
pub val: Vec<f64>,
}
impl UpperColumns {
/// From a symmetric matrix stored by full rows (row k's entries with column ≤ k are column k
/// of the upper triangle).
pub fn from_symmetric(a: &crate::sparse::Csr) -> UpperColumns {
let mut ptr = Vec::with_capacity(a.n + 1);
ptr.push(0);
let mut idx = Vec::new();
let mut val = Vec::new();
for k in 0..a.n {
let (cols, vals) = a.row(k);
for (c, v) in cols.iter().zip(vals) {
if *c <= k {
idx.push(*c);
val.push(*v);
}
}
ptr.push(idx.len());
}
UpperColumns { n: a.n, ptr, idx, val }
}
fn column(&self, k: usize) -> (&[usize], &[f64]) {
let (a, b) = (self.ptr.get(k).copied().unwrap_or(0), self.ptr.get(k + 1).copied().unwrap_or(0));
(self.idx.get(a..b).unwrap_or_default(), self.val.get(a..b).unwrap_or_default())
}
}
const NONE: usize = usize::MAX;
/// The elimination tree: parent of each column, `NONE` at roots.
fn etree(a: &UpperColumns) -> Vec<usize> {
let mut parent = vec![NONE; a.n];
let mut ancestor = vec![NONE; a.n];
for k in 0..a.n {
let (rows, _) = a.column(k);
for &i0 in rows {
let mut i = i0;
// Walk from i up to the root of its current subtree, compressing the path to k.
while i != NONE && i < k {
let next = ancestor.get(i).copied().unwrap_or(NONE);
if let Some(x) = ancestor.get_mut(i) {
*x = k;
}
if next == NONE
&& let Some(p) = parent.get_mut(i)
{
*p = k;
}
i = next;
}
}
}
parent
}
/// The pattern of row k of L (excluding the diagonal): the reach of column k's entries in the
/// elimination tree. Fills `stack[top..]` and returns `top`; `mark` holds `k` for visited
/// columns.
fn ereach(a: &UpperColumns, k: usize, parent: &[usize], stack: &mut [usize], mark: &mut [usize], path: &mut Vec<usize>) -> usize {
let n = a.n;
let mut top = n;
if let Some(m) = mark.get_mut(k) {
*m = k;
}
let (rows, _) = a.column(k);
for &i0 in rows {
let mut i = i0;
if i > k {
continue;
}
path.clear();
while i != NONE && mark.get(i).copied() != Some(k) {
path.push(i);
if let Some(m) = mark.get_mut(i) {
*m = k;
}
i = parent.get(i).copied().unwrap_or(NONE);
}
while let Some(v) = path.pop() {
top -= 1;
if let Some(s) = stack.get_mut(top) {
*s = v;
}
}
}
top
}
/// The elimination tree and the entry count of each column of L, without computing values.
fn symbolic(a: &UpperColumns) -> (Vec<usize>, Vec<usize>) {
let parent = etree(a);
let mut counts = vec![1usize; a.n];
let mut stack = vec![0; a.n];
let mut mark = vec![NONE; a.n];
let mut path = Vec::new();
for k in 0..a.n {
let top = ereach(a, k, &parent, &mut stack, &mut mark, &mut path);
for &j in stack.get(top..).unwrap_or_default() {
if let Some(c) = counts.get_mut(j) {
*c += 1;
}
}
}
(parent, counts)
}
/// A postorder of a forest given by parents, children in increasing order: `post[k]` is the
/// k-th node.
fn postorder(parent: &[usize]) -> Vec<usize> {
let n = parent.len();
// Children as linked lists in increasing order (built from the largest child down).
let mut head = vec![NONE; n];
let mut next = vec![NONE; n];
for j in (0..n).rev() {
let Some(&p) = parent.get(j) else { continue };
if let (Some(h), Some(x)) = (head.get(p).copied(), next.get_mut(j)) {
*x = h;
if let Some(h) = head.get_mut(p) {
*h = j;
}
}
}
let mut post = Vec::with_capacity(n);
let mut stack = Vec::new();
for root in (0..n).filter(|j| parent.get(*j) == Some(&NONE)) {
stack.push(root);
while let Some(&top) = stack.last() {
let child = head.get(top).copied().unwrap_or(NONE);
if child == NONE {
post.push(top);
stack.pop();
} else {
// Detach the first remaining child and descend into it.
if let Some(h) = head.get_mut(top) {
*h = next.get(child).copied().unwrap_or(NONE);
}
stack.push(child);
}
}
}
post
}
/// Why a factorization stopped.
#[derive(Debug, Clone, PartialEq)]
pub enum FactorError {
/// The pivot of this column is not positive (relative to the diagonal).
NotPositiveDefinite { column: usize },
/// The symmetric indefinite factorization ([`crate::ldlt`]) found no pivot in this column's
/// supernode that is not negligible next to the column's entries: the matrix is singular to
/// working precision there.
Singular { column: usize },
/// The factor would hold more entries than allowed.
TooLarge { entries: usize, limit: usize },
/// The memory for the factor could not be had.
OutOfMemory { entries: usize },
/// A broken internal invariant (a bug), reported instead of a panic.
Internal(String),
/// The command was cancelled ([`fieldcraft_model::progress`]) between supernodes.
Cancelled,
}
impl FactorError {
/// The column a numeric failure names.
fn column(&self) -> Option<usize> {
match self {
FactorError::NotPositiveDefinite { column } | FactorError::Singular { column } => Some(*column),
_ => None,
}
}
/// The same failure at another column.
fn at(self, column: usize) -> FactorError {
match self {
FactorError::NotPositiveDefinite { .. } => FactorError::NotPositiveDefinite { column },
FactorError::Singular { .. } => FactorError::Singular { column },
e => e,
}
}
}
/// One supernode's update to a later one: supernode `k`'s rows from position `p1` on reach the
/// later one, whose columns they meet at positions `p1..p2`.
#[derive(Debug, Clone, Copy)]
struct Update {
k: usize,
p1: usize,
p2: usize,
}
/// The supernodal structure (postorder numbering).
#[derive(Debug, Clone)]
pub(crate) struct Structure {
/// First column of each supernode, and `n` at the end.
first: Vec<usize>,
/// Row structure of each supernode (its own columns first, then the rows below), sorted.
rptr: Vec<usize>,
rows: Vec<usize>,
}
impl Structure {
pub(crate) fn cols(&self, s: usize) -> std::ops::Range<usize> {
self.first.get(s).copied().unwrap_or(0)..self.first.get(s + 1).copied().unwrap_or(0)
}
pub(crate) fn rows(&self, s: usize) -> &[usize] {
let (a, b) = (self.rptr.get(s).copied().unwrap_or(0), self.rptr.get(s + 1).copied().unwrap_or(0));
self.rows.get(a..b).unwrap_or_default()
}
/// Entries of the supernode's lower trapezoid.
pub(crate) fn size(&self, s: usize) -> usize {
trapezoid(self.rows(s).len(), self.cols(s).len())
}
}
/// Entries of an m-row, w-column lower trapezoid (column c holds rows c..m).
pub(crate) fn trapezoid(m: usize, w: usize) -> usize {
(0..w).map(|c| m.saturating_sub(c)).sum()
}
/// Where column c of an m-row trapezoid starts.
pub(crate) fn offset(m: usize, c: usize) -> usize {
c * m - c * c.saturating_sub(1) / 2
}
/// A dense lower trapezoid of m rows: one supernode's values, column by column, column c
/// holding rows c..m of the supernode's row structure.
pub(crate) struct Panel<'a> {
pub(crate) m: usize,
pub(crate) data: &'a [f64],
}
impl Panel<'_> {
/// L(r, c) for r ≥ c.
pub(crate) fn at(&self, r: usize, c: usize) -> f64 {
self.data.get(offset(self.m, c) + r - c).copied().unwrap_or(0.0)
}
/// Rows r..r+len of column c (r ≥ c).
pub(crate) fn column(&self, c: usize, r: usize, len: usize) -> &[f64] {
let start = offset(self.m, c) + r - c;
self.data.get(start..start + len).unwrap_or_default()
}
}
/// The pivots of a symmetric indefinite factor's block diagonal D, per column of a supernode
/// ([`crate::ldlt`]): a 1 × 1 pivot, or the first or second column of a 2 × 2 one. D's entries
/// sit on the trapezoid's diagonal, and a 2 × 2 pivot's off-diagonal entry below its first
/// column's diagonal (where L's unit block has a zero).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Pivot {
One,
First,
Second,
}
/// `row` (entries for the columns k0.. of a trapezoid) ← `row` D, with D the block diagonal the
/// trapezoid holds for those columns and `pivots` its pivots from column k0.
pub(crate) fn times_d(row: &mut [f64], l: &Panel, k0: usize, pivots: &[Pivot]) {
let mut k = 0;
while k < row.len() {
let d = |r: usize, c: usize| l.at(k0 + r, k0 + c);
if pivots.get(k) == Some(&Pivot::First) {
let (a, b) = (row.get(k).copied().unwrap_or(0.0), row.get(k + 1).copied().unwrap_or(0.0));
let (d11, d21, d22) = (d(k, k), d(k + 1, k), d(k + 1, k + 1));
if let Some(x) = row.get_mut(k) {
*x = a * d11 + b * d21;
}
if let Some(x) = row.get_mut(k + 1) {
*x = a * d21 + b * d22;
}
k += 2;
} else {
if let Some(x) = row.get_mut(k) {
*x *= d(k, k);
}
k += 1;
}
}
}
/// S(i, c) = Σ_k L(p + i, k) Y(p + c, k) over the columns k in `cols` of a trapezoid, for rows i
/// in 0..mu and columns c in 0..wu (meaningful for i ≥ c), each sum in column order. Y is L
/// (Cholesky), or L D with D the block diagonal the trapezoid holds when `pivots` (of the
/// columns `cols`) are given. Returned by row tiles: tile t holds rows t·TILE.. as
/// `tile[c * TILE + i]`.
pub(crate) fn products(l: &Panel, cols: std::ops::Range<usize>, p: usize, mu: usize, wu: usize, pivots: Option<&[Pivot]>) -> Vec<Vec<f64>> {
let (k0, kw) = (cols.start, cols.len());
// Rows p..p+wu of the columns, transposed so that column c's coefficients are contiguous.
let mut yt = vec![0.0; wu * kw];
for k in 0..kw {
for (c, y) in l.column(k0 + k, p, wu).iter().enumerate() {
if let Some(slot) = yt.get_mut(c * kw + k) {
*slot = *y;
}
}
}
if let Some(pivots) = pivots {
for row in yt.chunks_mut(kw.max(1)) {
times_d(row, l, k0, pivots);
}
}
let tiles = mu.div_ceil(TILE);
let tile = |t: usize| -> Vec<f64> {
let i0 = t * TILE;
let h = TILE.min(mu - i0);
// The tile's rows of every column, padded to TILE rows.
let mut x = vec![0.0; kw * TILE];
for k in 0..kw {
if let Some(dst) = x.get_mut(k * TILE..k * TILE + h) {
dst.copy_from_slice(l.column(k0 + k, p + i0, h));
}
}
let cols = wu.min(i0 + h);
let mut out = vec![0.0; cols * TILE];
for c in 0..cols {
let mut acc = [0.0f64; TILE];
let ys = yt.get(c * kw..(c + 1) * kw).unwrap_or_default();
for (xs, b) in x.as_chunks::<TILE>().0.iter().zip(ys) {
for (a, xi) in acc.iter_mut().zip(xs) {
*a += xi * b;
}
}
if let Some(dst) = out.get_mut(c * TILE..(c + 1) * TILE) {
dst.copy_from_slice(&acc);
}
}
out
};
if mu.saturating_mul(wu).saturating_mul(kw) >= PARALLEL_WORK { parallel::map(tiles, tile) } else { (0..tiles).map(tile).collect() }
}
/// own(rel(i), rel(c)) −= S(i, c) for i ≥ c, with S in the tiles of [`products`].
pub(crate) fn subtract(own: &mut [f64], m: usize, tiles: &[Vec<f64>], mu: usize, wu: usize, rel: impl Fn(usize) -> usize) {
for (t, tile) in tiles.iter().enumerate() {
let i0 = t * TILE;
let h = TILE.min(mu - i0);
for c in 0..wu.min(i0 + h) {
let cc = rel(c);
for i in i0.max(c)..i0 + h {
let rr = rel(i);
if let (Some(x), Some(s)) = (own.get_mut(offset(m, cc) + rr - cc), tile.get(c * TILE + i - i0)) {
*x -= s;
}
}
}
}
}
/// Rows c1..m of the panel c0..c1 solved against its factored diagonal block, by tiles of
/// [`TILE`] rows (`tile[c * TILE + i]`): L(r, c) −= L(r, k) L(c, k) for the panel's k < c in
/// order, then ÷ L(c, c).
fn solve_below(l: &Panel, c0: usize, c1: usize) -> Vec<Vec<f64>> {
let (m, pw) = (l.m, c1 - c0);
let tiles = (m - c1).div_ceil(TILE);
let tile = |t: usize| -> Vec<f64> {
let i0 = c1 + t * TILE;
let h = TILE.min(m - i0);
let mut buf = vec![0.0; pw * TILE];
for c in 0..pw {
if let Some(dst) = buf.get_mut(c * TILE..c * TILE + h) {
dst.copy_from_slice(l.column(c0 + c, i0, h));
}
}
for c in 0..pw {
let (done, rest) = buf.split_at_mut(c * TILE);
let Some(target) = rest.get_mut(..TILE) else { continue };
for (k, src) in done.as_chunks::<TILE>().0.iter().enumerate() {
let lck = l.at(c0 + c, c0 + k);
for (y, x) in target.iter_mut().zip(src) {
*y -= x * lck;
}
}
let d = l.at(c0 + c, c0 + c);
for y in target.iter_mut() {
*y /= d;
}
}
buf
};
if (m - c1).saturating_mul(pw).saturating_mul(pw) >= PARALLEL_WORK { parallel::map(tiles, tile) } else { (0..tiles).map(tile).collect() }
}
/// The matrix in postorder by lower columns: column j holds A(i, j) for i ≥ j, rows sorted.
pub(crate) struct Lower {
ptr: Vec<usize>,
idx: Vec<usize>,
val: Vec<f64>,
diag: Vec<f64>,
/// The largest magnitude in each row (= column) of the whole symmetric matrix.
pub(crate) scale: Vec<f64>,
}
impl Lower {
fn new(a: &UpperColumns, inverse: &[usize]) -> Lower {
let n = a.n;
let new = |i: usize| inverse.get(i).copied().unwrap_or(0);
let mut ptr = vec![0usize; n + 1];
for k in 0..n {
for &i in a.column(k).0 {
if let Some(c) = ptr.get_mut(new(i).min(new(k)) + 1) {
*c += 1;
}
}
}
for j in 0..n {
let before = ptr.get(j).copied().unwrap_or(0);
if let Some(c) = ptr.get_mut(j + 1) {
*c += before;
}
}
let mut pairs = vec![(0usize, 0.0f64); ptr.last().copied().unwrap_or(0)];
let mut next = ptr.clone();
let mut diag = vec![0.0; n];
let mut scale = vec![0.0f64; n];
for k in 0..n {
let (rows, vals) = a.column(k);
for (&i, &v) in rows.iter().zip(vals) {
let (r, c) = (new(i).max(new(k)), new(i).min(new(k)));
if r == c
&& let Some(d) = diag.get_mut(c)
{
*d = v;
}
for x in [r, c] {
if let Some(s) = scale.get_mut(x) {
*s = s.max(v.abs());
}
}
if let Some(slot) = next.get_mut(c) {
if let Some(p) = pairs.get_mut(*slot) {
*p = (r, v);
}
*slot += 1;
}
}
}
for j in 0..n {
let (s, e) = (ptr.get(j).copied().unwrap_or(0), ptr.get(j + 1).copied().unwrap_or(0));
if let Some(col) = pairs.get_mut(s..e) {
col.sort_by_key(|p| p.0);
}
}
let (idx, val) = pairs.into_iter().unzip();
Lower { ptr, idx, val, diag, scale }
}
fn column(&self, j: usize) -> (&[usize], &[f64]) {
let (a, b) = (self.ptr.get(j).copied().unwrap_or(0), self.ptr.get(j + 1).copied().unwrap_or(0));
(self.idx.get(a..b).unwrap_or_default(), self.val.get(a..b).unwrap_or_default())
}
}
/// What a supernode's factorization reads: the matrix, the structure and the updates each
/// supernode receives.
pub(crate) struct Context<'a> {
pub(crate) a: &'a Lower,
pub(crate) s: &'a Structure,
updates: &'a [Vec<Update>],
}
/// What the numeric factorization of one kind keeps per supernode.
pub(crate) trait Supernode: Send + Sync {
/// The supernode's values: its lower trapezoid by columns.
fn values(&self) -> &[f64];
/// The pivots of D (symmetric indefinite factors), `None` for Cholesky.
fn pivots(&self) -> Option<&[Pivot]>;
}
impl Supernode for Vec<f64> {
fn values(&self) -> &[f64] {
self
}
fn pivots(&self) -> Option<&[Pivot]> {
None
}
}
fn internal(what: &str) -> FactorError {
FactorError::Internal(format!("supernodal factorization: {what}"))
}
impl Context<'_> {
/// Supernode `j` assembled into `own`: its columns of A minus the update L_K Y_Kᵀ of every
/// descendant K that reaches it (Y = L, or L D), in supernode order; the descendants are
/// `done[k - base]`.
pub(crate) fn assemble<T: Supernode>(&self, j: usize, done: &[T], base: usize, own: &mut [f64]) -> std::result::Result<(), FactorError> {
let rows = self.s.rows(j);
let cols = self.s.cols(j);
let (m, w, f) = (rows.len(), cols.len(), cols.start);
if own.len() != trapezoid(m, w) {
return Err(internal("supernode size"));
}
own.fill(0.0);
// The columns of A.
for c in 0..w {
let (ai, av) = self.a.column(f + c);
for (r, v) in ai.iter().zip(av) {
let at = rows.binary_search(r).map_err(|_| internal("an entry of A outside the structure"))?;
if at < c {
return Err(internal("an entry above the diagonal"));
}
if let Some(x) = own.get_mut(offset(m, c) + at - c) {
*x = *v;
}
}
}
// The descendants' updates, in supernode order.
let mut relative = Vec::new();
for u in self.updates.get(j).map(Vec::as_slice).unwrap_or_default() {
let done_k = done.get(u.k.wrapping_sub(base)).ok_or_else(|| internal("an update from outside the subtree"))?;
let lk = done_k.values();
let (kr, kw) = (self.s.rows(u.k), self.s.cols(u.k).len());
let below = kr.get(u.p1..).unwrap_or_default();
let (mu, wu) = (below.len(), u.p2 - u.p1);
// Positions of K's rows in J's structure (both sorted).
relative.clear();
let mut at = 0;
for r in below {
while rows.get(at).is_some_and(|x| x < r) {
at += 1;
}
if rows.get(at) != Some(r) {
return Err(internal("an update row outside the structure"));
}
relative.push(at);
}
let tiles = products(&Panel { m: kr.len(), data: lk }, 0..kw, u.p1, mu, wu, done_k.pivots());
subtract(own, m, &tiles, mu, wu, |i| relative.get(i).copied().unwrap_or(0));
}
Ok(())
}
/// Factor supernode `j` into `own`; its descendants' values are `done[k - base]`. Fails with
/// the first column (postorder) whose pivot is not positive.
fn supernode(&self, j: usize, done: &[Vec<f64>], base: usize, own: &mut [f64]) -> std::result::Result<(), FactorError> {
self.assemble(j, done, base, own)?;
let rows = self.s.rows(j);
let cols = self.s.cols(j);
let (m, w, f) = (rows.len(), cols.len(), cols.start);
// Dense factorization by panels.
for c0 in (0..w).step_by(PANEL) {
let c1 = (c0 + PANEL).min(w);
let pw = c1 - c0;
if c0 > 0 {
// The panel minus its products with the columns before it.
let tiles = products(&Panel { m, data: own }, 0..c0, c0, m - c0, pw, None);
subtract(own, m, &tiles, m - c0, pw, |i| c0 + i);
}
// The diagonal block, column by column.
for c in c0..c1 {
let dst = offset(m, c);
for k in c0..c {
let src = offset(m, k) + c - k;
let lck = own.get(src).copied().unwrap_or(0.0);
for r in 0..c1 - c {
let x = own.get(src + r).copied().unwrap_or(0.0);
if let Some(y) = own.get_mut(dst + r) {
*y -= x * lck;
}
}
}
let d = own.get(dst).copied().unwrap_or(0.0);
let diag = self.a.diag.get(f + c).copied().unwrap_or(0.0);
if !(d > PIVOT_TOLERANCE * diag.abs()) || !d.is_finite() {
return Err(FactorError::NotPositiveDefinite { column: f + c });
}
let d = d.sqrt();
if let Some(x) = own.get_mut(dst) {
*x = d;
}
for x in own.get_mut(dst + 1..dst + c1 - c).unwrap_or_default() {
*x /= d;
}
}
// The rows below the panel, by tiles.
if c1 < m {
let solved = solve_below(&Panel { m, data: own }, c0, c1);
for (t, tile) in solved.iter().enumerate() {
let i0 = c1 + t * TILE;
let h = TILE.min(m - i0);
for c in 0..pw {
let start = offset(m, c0 + c) + i0 - (c0 + c);
if let (Some(dst), Some(src)) = (own.get_mut(start..start + h), tile.get(c * TILE..c * TILE + h)) {
dst.copy_from_slice(src);
}
}
}
}
}
Ok(())
}
}
/// The symbolic factorization shared by the Cholesky and the symmetric indefinite (LDLᵀ,
/// [`crate::ldlt`]) factors: the postorder, the supernodes and their row structures, the updates
/// each supernode receives, the matrix in that order, and the subtree tasks of the numeric
/// phase.
pub(crate) struct Symbolic {
pub(crate) n: usize,
/// `perm[k]`: the original column that is column k of the factor.
pub(crate) perm: Vec<usize>,
pub(crate) structure: Structure,
a: Lower,
updates: Vec<Vec<Update>>,
/// Subtrees factored as parallel tasks, and the supernodes above them, in order.
tasks: Vec<std::ops::Range<usize>>,
top: Vec<usize>,
/// Entries of L.
pub(crate) entries: usize,
}
impl Symbolic {
/// The structure of the factor of `a`, refused before any numeric memory is taken when the
/// factor would hold more than `max_entries` entries.
pub(crate) fn new(a: &UpperColumns, max_entries: usize) -> std::result::Result<Symbolic, FactorError> {
let n = a.n;
let internal = |what: &str| FactorError::Internal(format!("supernodal structure: {what}"));
let (parent0, counts0) = symbolic(a);
let total: usize = counts0.iter().sum();
if total > max_entries {
return Err(FactorError::TooLarge { entries: total, limit: max_entries });
}
// Postorder: the same tree and counts, renumbered.
let perm = postorder(&parent0);
if perm.len() != n {
return Err(internal("postorder"));
}
let mut inverse = vec![0usize; n];
for (k, &j) in perm.iter().enumerate() {
if let Some(x) = inverse.get_mut(j) {
*x = k;
}
}
let parent: Vec<usize> =
perm.iter().map(|&j| parent0.get(j).copied().filter(|p| *p != NONE).and_then(|p| inverse.get(p).copied()).unwrap_or(NONE)).collect();
let counts: Vec<usize> = perm.iter().map(|&j| counts0.get(j).copied().unwrap_or(1)).collect();
let a = Lower::new(a, &inverse);
// Fundamental supernodes: column j joins column j − 1's supernode when it is j − 1's
// parent, j − 1 is its only child, and their structures nest.
let mut children = vec![0usize; n];
for &p in &parent {
if let Some(c) = children.get_mut(p) {
*c += 1;
}
}
let mut first = vec![0usize];
for j in 1..n {
let joins = parent.get(j - 1) == Some(&j) && children.get(j) == Some(&1) && counts.get(j - 1).copied() == counts.get(j).map(|c| c + 1);
if !joins {
first.push(j);
}
}
if n > 0 {
first.push(n);
}
let ns = first.len().saturating_sub(1);
let mut snode = vec![0usize; n];
for s in 0..ns {
for j in first.get(s).copied().unwrap_or(0)..first.get(s + 1).copied().unwrap_or(0) {
if let Some(x) = snode.get_mut(j) {
*x = s;
}
}
}
// The supernodal tree and the row structures: a supernode's rows are its columns, the
// rows of A below them and its children's rows past their own columns.
let sparent: Vec<usize> = (0..ns)
.map(|s| {
let last = first.get(s + 1).copied().unwrap_or(1).saturating_sub(1);
parent.get(last).copied().filter(|p| *p != NONE).and_then(|p| snode.get(p).copied()).unwrap_or(NONE)
})
.collect();
let mut kids: Vec<Vec<usize>> = vec![Vec::new(); ns];
for (s, &p) in sparent.iter().enumerate() {
if let Some(k) = kids.get_mut(p) {
k.push(s);
}
}
let mut rptr = vec![0usize];
let mut rows: Vec<usize> = Vec::new();
let mut mark = vec![NONE; n];
let mut set = Vec::new();
for s in 0..ns {
let (f, l) = (first.get(s).copied().unwrap_or(0), first.get(s + 1).copied().unwrap_or(0));
set.clear();
let mut add = |r: usize, set: &mut Vec<usize>| {
if let Some(x) = mark.get_mut(r)
&& *x != s
{
*x = s;
set.push(r);
}
};
for j in f..l {
add(j, &mut set);
for &r in a.column(j).0 {
add(r, &mut set);
}
}
for &c in kids.get(s).map(Vec::as_slice).unwrap_or_default() {
let (cs, ce) = (rptr.get(c).copied().unwrap_or(0), rptr.get(c + 1).copied().unwrap_or(0));
let cw = first.get(c + 1).copied().unwrap_or(0) - first.get(c).copied().unwrap_or(0);
for &r in rows.get(cs + cw..ce).unwrap_or_default() {
add(r, &mut set);
}
}
set.sort_unstable();
if set.len() != counts.get(f).copied().unwrap_or(0) || set.first() != Some(&f) {
return Err(internal("a supernode's rows differ from its column count"));
}
rows.extend_from_slice(&set);
rptr.push(rows.len());
}
let structure = Structure { first, rptr, rows };
// The updates each supernode receives, from its descendants in increasing order.
let mut updates: Vec<Vec<Update>> = vec![Vec::new(); ns];
for k in 0..ns {
let kr = structure.rows(k);
let mut p = structure.cols(k).len();
while let Some(target) = kr.get(p).and_then(|r| snode.get(*r)).copied() {
let end = structure.cols(target).end;
let mut q = p;
while kr.get(q).is_some_and(|r| *r < end) {
q += 1;
}
if let Some(list) = updates.get_mut(target) {
list.push(Update { k, p1: p, p2: q });
}
p = q;
}
}
// Tasks: subtrees (contiguous ranges of supernodes in postorder) of at most a share of the
// work each; the supernodes above them are factored afterwards, in order.
let mut subtree: Vec<usize> = (0..ns)
.map(|s| {
let (m, w) = (structure.rows(s).len(), structure.cols(s).len());
(0..w).map(|c| (m - c).saturating_mul(m - c)).fold(0usize, usize::saturating_add)
})
.collect();
let mut start: Vec<usize> = (0..ns).collect();
for s in 0..ns {
if let Some(&p) = sparent.get(s)
&& p != NONE
{
let (ws, ss) = (subtree.get(s).copied().unwrap_or(0), start.get(s).copied().unwrap_or(s));
if let Some(x) = subtree.get_mut(p) {
*x = x.saturating_add(ws);
}
if let Some(x) = start.get_mut(p) {
*x = (*x).min(ss);
}
}
}
let all = (0..ns).filter(|s| sparent.get(*s) == Some(&NONE)).filter_map(|s| subtree.get(s).copied()).fold(0, usize::saturating_add);
let share = (all / SUBTREE_SHARE).max(PARALLEL_WORK);
let fits = |s: usize| subtree.get(s).is_some_and(|w| *w <= share);
let mut tasks: Vec<std::ops::Range<usize>> = Vec::new();
let mut top: Vec<usize> = Vec::new();
let mut s = 0;
while s < ns {
// The largest fitting subtree whose first supernode is s.
let mut root = NONE;
let mut r = s;
while r != NONE && start.get(r) == Some(&s) && fits(r) {
root = r;
r = sparent.get(r).copied().unwrap_or(NONE);
}
if root == NONE {
top.push(s);
s += 1;
} else {
tasks.push(s..root + 1);
s = root + 1;
}
}
Ok(Symbolic { n, perm, structure, a, updates, tasks, top, entries: total })
}
/// Supernodes.
pub(crate) fn supernodes(&self) -> usize {
self.structure.first.len().saturating_sub(1)
}
/// The original column of factor column `column`.
pub(crate) fn original(&self, column: usize) -> usize {
self.perm.get(column).copied().unwrap_or(column)
}
/// Zeroed memory for every supernode's trapezoid, taken without aborting.
pub(crate) fn allocate(&self) -> std::result::Result<Vec<Vec<f64>>, FactorError> {
let ns = self.supernodes();
let mut values: Vec<Vec<f64>> = Vec::new();
values.try_reserve_exact(ns).map_err(|_| FactorError::OutOfMemory { entries: self.entries })?;
for s in 0..ns {
let mut v = Vec::new();
v.try_reserve_exact(self.structure.size(s)).map_err(|_| FactorError::OutOfMemory { entries: self.entries })?;
v.resize(self.structure.size(s), 0.0);
values.push(v);
}
Ok(values)
}
/// Run `supernode(context, j, done, base, own)` over every supernode, children before
/// parents: the subtree tasks in parallel, each through its supernodes in order, then the
/// supernodes above them in order. A numeric failure is the first failing column in
/// postorder (the one a pass in order would meet first, whichever thread meets it), named by
/// its original column.
pub(crate) fn factor<T: Supernode>(
&self,
values: &mut [T],
supernode: impl Fn(&Context, usize, &[T], usize, &mut T) -> std::result::Result<(), FactorError> + Sync,
) -> std::result::Result<(), FactorError> {
let context = Context { a: &self.a, s: &self.structure, updates: &self.updates };
// The tasks run in parallel, each through its supernodes in order.
let mut pieces: Vec<(std::ops::Range<usize>, &mut [T], Option<FactorError>)> = Vec::with_capacity(self.tasks.len());
let mut rest: &mut [T] = values;
let mut at = 0;
for range in &self.tasks {
let (_, tail) = std::mem::take(&mut rest).split_at_mut(range.start - at);
let (piece, tail) = tail.split_at_mut(range.len());
pieces.push((range.clone(), piece, None));
rest = tail;
at = range.end;
}
let broken = std::sync::Mutex::new(None::<FactorError>);
// The pool's threads do not report: they ask the caller's progress whether to stop.
let progress = fieldcraft_model::progress::current();
let cancelled = || progress.as_ref().is_some_and(|p| p.is_cancelled());
parallel::for_each_mut(&mut pieces, |_, (range, piece, failed)| {
for j in range.clone() {
if cancelled() {
if let Ok(mut b) = broken.lock() {
b.get_or_insert(FactorError::Cancelled);
}
break;
}
let (done, own) = piece.split_at_mut(j - range.start);
let Some(own) = own.first_mut() else { break };
match supernode(&context, j, done, range.start, own) {
Ok(()) => {}
Err(e) if e.column().is_some() => {
*failed = Some(e);
break;
}
Err(e) => {
if let Ok(mut b) = broken.lock() {
b.get_or_insert(e);
}
break;
}
}
}
});
if let Some(e) = broken.into_inner().unwrap_or_else(std::sync::PoisonError::into_inner) {
return Err(e);
}
// The first failing column in postorder is the one a pass in order would meet first:
// the supernodes above the tasks run up to it.
let earliest = pieces.iter_mut().filter_map(|p| p.2.take()).min_by_key(|e| e.column());
drop(pieces);
let named = |e: FactorError| match e.column() {
Some(c) => e.at(self.original(c)),
None => e,
};
for &j in &self.top {
if cancelled() {
return Err(FactorError::Cancelled);
}
if earliest.as_ref().and_then(FactorError::column).is_some_and(|c| self.structure.cols(j).start > c) {
break;
}
let (done, own) = values.split_at_mut(j);
let Some(own) = own.first_mut() else { break };
supernode(&context, j, done, 0, own).map_err(named)?;
}
match earliest {
Some(e) => Err(named(e)),
None => Ok(()),
}
}
}
/// A Cholesky factor: L in supernodes, for the matrix with its columns in postorder.
#[derive(Debug, Clone)]
pub struct Factor {
pub n: usize,
/// `perm[k]`: the original column that is column k of the factor.
perm: Vec<usize>,
structure: Structure,
/// The values of each supernode (a lower trapezoid by columns).
values: Vec<Vec<f64>>,
entries: usize,
}
impl Factor {
pub fn new(a: &UpperColumns, max_entries: usize) -> std::result::Result<Factor, FactorError> {
Factor::from_symbolic(&Symbolic::new(a, max_entries)?)
}
pub(crate) fn from_symbolic(symbolic: &Symbolic) -> std::result::Result<Factor, FactorError> {
let mut values = symbolic.allocate()?;
symbolic.factor(&mut values, |context, j, done, base, own| context.supernode(j, done, base, own))?;
Ok(Factor { n: symbolic.n, perm: symbolic.perm.clone(), structure: symbolic.structure.clone(), values, entries: symbolic.entries })
}
/// Entries in L.
pub fn entries(&self) -> usize {
self.entries
}
/// Solve A x = b in place.
pub fn solve(&self, b: &mut [f64]) -> Result<()> {
if b.len() != self.n {
return Err(Error::Internal("right-hand side length".into()));
}
let mut x: Vec<f64> = self.perm.iter().map(|&j| b.get(j).copied().unwrap_or(0.0)).collect();
let s = &self.structure;
// L y = b, by columns.
for (k, l) in self.values.iter().enumerate() {
let rows = s.rows(k);
let m = rows.len();
for (c, j) in s.cols(k).enumerate() {
let start = offset(m, c);
let yj = x.get(j).copied().unwrap_or(0.0) / l.get(start).copied().unwrap_or(1.0);
if let Some(v) = x.get_mut(j) {
*v = yj;
}
for (r, v) in rows.get(c + 1..).unwrap_or_default().iter().zip(l.get(start + 1..).unwrap_or_default()) {
if let Some(t) = x.get_mut(*r) {
*t -= v * yj;
}
}
}
}
// Lᵀ x = y.
for (k, l) in self.values.iter().enumerate().rev() {
let rows = s.rows(k);
let m = rows.len();
for (c, j) in s.cols(k).enumerate().rev() {
let start = offset(m, c);
let mut sum = x.get(j).copied().unwrap_or(0.0);
for (r, v) in rows.get(c + 1..).unwrap_or_default().iter().zip(l.get(start + 1..).unwrap_or_default()) {
sum -= v * x.get(*r).copied().unwrap_or(0.0);
}
if let Some(t) = x.get_mut(j) {
*t = sum / l.get(start).copied().unwrap_or(1.0);
}
}
}
for (&j, v) in self.perm.iter().zip(&x) {
if let Some(t) = b.get_mut(j) {
*t = *v;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::Csr;
fn dense_to_csr(a: &[Vec<f64>]) -> Csr {
let n = a.len();
let mut ptr = vec![0];
let mut idx = Vec::new();
let mut val = Vec::new();
for row in a {
for (j, v) in row.iter().enumerate() {
if *v != 0.0 {
idx.push(j);
val.push(*v);
}
}
ptr.push(idx.len());
}
Csr { n, ptr, idx, val }
}
fn check_solve(csr: &Csr, f: &Factor, tol: f64) {
let n = csr.n;
let x: Vec<f64> = (0..n).map(|i| (i as f64 * 0.37).sin()).collect();
let mut b = vec![0.0; n];
csr.mul(&x, &mut b);
f.solve(&mut b).unwrap();
let err = b.iter().zip(&x).map(|(p, q)| (p - q).abs()).fold(0.0, f64::max);
assert!(err < tol, "{err}");
}
#[test]
fn factor_and_solve_a_sparse_spd_matrix() {
// A 2D Laplacian-like matrix on a 7 × 5 grid plus a diagonal shift: sparse with fill.
let (w, h) = (7, 5);
let n = w * h;
let mut a = vec![vec![0.0; n]; n];
for i in 0..w {
for j in 0..h {
let k = i * h + j;
a[k][k] = 4.5;
if i + 1 < w {
a[k][k + h] = -1.0;
a[k + h][k] = -1.0;
}
if j + 1 < h {
a[k][k + 1] = -1.0;
a[k + 1][k] = -1.0;
}
}
}
let csr = dense_to_csr(&a);
let f = Factor::new(&UpperColumns::from_symmetric(&csr), usize::MAX).unwrap();
check_solve(&csr, &f, 1e-13);
// The cap is checked before the numeric factorization.
assert!(matches!(Factor::new(&UpperColumns::from_symmetric(&csr), 10), Err(FactorError::TooLarge { .. })));
// A singular matrix (a pure Laplacian has the constant vector in its null space).
let mut sing = a.clone();
for (k, row) in sing.iter_mut().enumerate() {
let off: f64 = row.iter().enumerate().filter(|(j, _)| *j != k).map(|(_, v)| -v).sum();
row[k] = off;
}
let e = Factor::new(&UpperColumns::from_symmetric(&dense_to_csr(&sing)), usize::MAX).unwrap_err();
assert_eq!(e, FactorError::NotPositiveDefinite { column: n - 1 });
// A cancelled command stops between supernodes, in Cholesky and LDLᵀ alike.
let p = fieldcraft_model::progress::Progress::new();
p.cancel();
fieldcraft_model::progress::scoped(Some(p), || {
let upper = UpperColumns::from_symmetric(&csr);
assert_eq!(Factor::new(&upper, usize::MAX).unwrap_err(), FactorError::Cancelled);
assert_eq!(crate::ldlt::LdltFactor::new(&upper, usize::MAX).unwrap_err(), FactorError::Cancelled);
});
// Empty and 1 × 1 matrices.
let empty = Csr { n: 0, ptr: vec![0], idx: vec![], val: vec![] };
let f = Factor::new(&UpperColumns::from_symmetric(&empty), 10).unwrap();
assert_eq!(f.entries(), 0);
f.solve(&mut []).unwrap();
let f = Factor::new(&UpperColumns::from_symmetric(&dense_to_csr(&[vec![4.0]])), 10).unwrap();
let mut b = [2.0];
f.solve(&mut b).unwrap();
assert_eq!(b, [0.5]);
}
/// Two 3D grids (3 coupled unknowns per point, in grid order) joined by a dense separator
/// ordered last, as nested dissection orders them, and a separate 5 × 5 block: subtrees run
/// as parallel tasks, the separator is a supernode of several panels whose kernels run in
/// parallel tiles. The factor solves the system and is the same bit for bit on any number of
/// threads.
#[test]
fn supernodes_panels_and_threads() {
let (nx, ny, nz) = (8, 8, 6);
let point = |i: usize, j: usize, k: usize| i + nx * (j + ny * k);
let per = 3 * nx * ny * nz;
let sep = 400;
let n = 2 * per + sep + 5;
let mut rows: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
let pair = |rows: &mut Vec<Vec<(usize, f64)>>, a: usize, b: usize, v: f64| {
rows[a].push((b, v));
rows[b].push((a, v));
};
for base in [0, per] {
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let p = base + 3 * point(i, j, k);
for a in 0..3 {
rows[p + a].push((p + a, 7.0));
for b in a + 1..3 {
pair(&mut rows, p + a, p + b, 0.25);
}
}
let next = [
(i + 1 < nx).then(|| point(i + 1, j, k)),
(j + 1 < ny).then(|| point(i, j + 1, k)),
(k + 1 < nz).then(|| point(i, j, k + 1)),
];
for q in next.into_iter().flatten() {
for a in 0..3 {
pair(&mut rows, p + a, base + 3 * q + a, -1.0);
}
}
}
}
}
}
let s0 = 2 * per;
for a in 0..sep {
rows[s0 + a].push((s0 + a, 20.0));
for b in a + 1..sep {
pair(&mut rows, s0 + a, s0 + b, 0.5 / (1.0 + (b - a) as f64).powi(2));
}
pair(&mut rows, s0 + a, (7 * a) % per, -0.1);
pair(&mut rows, s0 + a, per + (11 * a) % per, -0.1);
}
let d0 = s0 + sep;
for a in 0..5 {
rows[d0 + a].push((d0 + a, 10.0));
for b in a + 1..5 {
pair(&mut rows, d0 + a, d0 + b, 1.0 / (1.0 + (a + b) as f64));
}
}
let mut ptr = vec![0];
let mut idx = Vec::new();
let mut val = Vec::new();
for row in &mut rows {
row.sort_by_key(|e| e.0);
for (c, v) in row.iter() {
idx.push(*c);
val.push(*v);
}
ptr.push(idx.len());
}
let csr = Csr { n, ptr, idx, val };
let upper = UpperColumns::from_symmetric(&csr);
let one = parallel::with_threads(1, || Factor::new(&upper, usize::MAX)).unwrap();
let supernodes = one.structure.first.len() - 1;
let widest = (0..supernodes).map(|s| one.structure.cols(s).len()).max().unwrap();
assert!(widest > 2 * PANEL, "a supernode spans panels: {widest}");
check_solve(&csr, &one, 1e-12);
for threads in [2, 7] {
let other = parallel::with_threads(threads, || Factor::new(&upper, usize::MAX)).unwrap();
assert!(one.values.iter().flatten().zip(other.values.iter().flatten()).all(|(a, b)| a.to_bits() == b.to_bits()), "{threads} threads");
}
}
#[test]
fn postorder_lists_children_first() {
assert_eq!(postorder(&[2, 2, 4, 4, NONE, NONE]), vec![0, 1, 2, 3, 4, 5]);
assert_eq!(postorder(&[4, 3, 3, 4, NONE]), vec![0, 1, 2, 3, 4]);
assert_eq!(postorder(&[NONE, 0]), vec![1, 0]);
assert_eq!(postorder(&[3, NONE, 1, NONE]), vec![2, 1, 0, 3]);
}
}