use dyn_stack::{MemBuffer, MemStack};
use faer::diag::{Diag, DiagRef};
use faer::linalg::solvers::{self, Solve};
pub use faer::linalg::solvers::{
Lblt as FaerLblt, Ldlt as FaerLdlt, Llt as FaerLlt, Solve as FaerSolve,
};
use faer::linalg::svd::{self, ComputeSvdVectors};
use faer::prelude::ReborrowMut;
use faer::{Conj, Mat, MatMut, MatRef, Par, Side, Unbind, get_global_parallelism};
use ndarray::{Array1, Array2, ArrayBase, ArrayView1, ArrayViewMut1, Data, Ix1, Ix2};
use std::marker::PhantomData;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicU64, Ordering};
use thiserror::Error;
pub fn symmetric_matvec_into(
matrix: &Array2<f64>,
vector: &[f64],
output: &mut [f64],
) -> Result<(), String> {
let n = matrix.nrows();
if matrix.ncols() != n || vector.len() != n || output.len() != n {
return Err(format!(
"symmetric matvec shape mismatch: matrix={:?}, vector={}, output={}",
matrix.dim(),
vector.len(),
output.len()
));
}
fast_av_standard_view_into(
matrix,
&ArrayView1::from(vector),
ArrayViewMut1::from(output),
);
Ok(())
}
const RRQR_RANK_ALPHA: f64 = 100.0;
thread_local! {
static NESTED_PARALLEL_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
struct NestedParallelGuard;
impl NestedParallelGuard {
#[inline]
fn enter() -> Self {
NESTED_PARALLEL_DEPTH.with(|depth| depth.set(depth.get().saturating_add(1)));
Self
}
}
impl Drop for NestedParallelGuard {
#[inline]
fn drop(&mut self) {
NESTED_PARALLEL_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
#[inline]
pub fn with_nested_parallel<T>(body: impl FnOnce() -> T) -> T {
let guard = NestedParallelGuard::enter();
let out = body();
drop(guard);
out
}
#[inline]
pub fn in_nested_parallel_region() -> bool {
NESTED_PARALLEL_DEPTH.with(|depth| depth.get() > 0)
}
static EIGH_CALLS: AtomicU64 = AtomicU64::new(0);
static EIGH_NANOS: AtomicU64 = AtomicU64::new(0);
static EIGH_SEQ_CALLS: AtomicU64 = AtomicU64::new(0);
static EIGH_MAX_DIM: AtomicU64 = AtomicU64::new(0);
thread_local! {
static EIGH_THREAD: std::cell::Cell<EighCensus> = const {
std::cell::Cell::new(EighCensus {
calls: 0,
sequential_calls: 0,
max_dim: 0,
nanos: 0,
})
};
}
fn record_thread_eigh(sequential: bool, dim: u64, nanos: u64) {
EIGH_THREAD.with(|cell| {
let mut census = cell.get();
census.calls += 1;
if sequential {
census.sequential_calls += 1;
}
census.max_dim = census.max_dim.max(dim);
census.nanos += nanos;
cell.set(census);
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EighCensus {
pub calls: u64,
pub sequential_calls: u64,
pub max_dim: u64,
pub nanos: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParallelismSnapshot {
pub rayon_current_num_threads: usize,
pub faer_global_sequential: bool,
pub faer_global_degree: usize,
pub faer_sequential_scope_depth: usize,
pub process_available_parallelism: Option<usize>,
}
impl ParallelismSnapshot {
pub fn capture() -> Self {
Self::from_parts(
get_global_parallelism(),
rayon::current_num_threads(),
faer_sequential_scope_depth(),
std::thread::available_parallelism().ok().map(|n| n.get()),
)
}
pub fn from_parts(
faer_global: Par,
rayon_current_num_threads: usize,
faer_sequential_scope_depth: usize,
process_available_parallelism: Option<usize>,
) -> Self {
Self {
rayon_current_num_threads,
faer_global_sequential: faer_global == Par::Seq,
faer_global_degree: faer_global.degree(),
faer_sequential_scope_depth,
process_available_parallelism,
}
}
pub fn inconsistency(&self) -> Option<String> {
if self.rayon_current_num_threads == 0 {
return Some("rayon reports a pool of zero threads".to_string());
}
if self.faer_global_degree == 0 {
return Some("faer's global parallelism has degree zero".to_string());
}
if self.faer_global_sequential && self.faer_global_degree != 1 {
return Some(format!(
"faer is sequential but reports degree {}",
self.faer_global_degree
));
}
if self.faer_sequential_scope_depth > 0 && !self.faer_global_sequential {
return Some(format!(
"{} live FaerSequentialScope guard(s) but faer's global parallelism is not sequential",
self.faer_sequential_scope_depth
));
}
if self.process_available_parallelism == Some(0) {
return Some("this process reports zero available cores".to_string());
}
None
}
}
impl std::fmt::Display for ParallelismSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"rayon_current_num_threads={} | faer_global_sequential={} | \
faer_global_degree={} | faer_sequential_scope_depth={} | \
process_available_parallelism={}",
self.rayon_current_num_threads,
self.faer_global_sequential,
self.faer_global_degree,
self.faer_sequential_scope_depth,
match self.process_available_parallelism {
Some(cores) => cores.to_string(),
None => "unavailable".to_string(),
},
)
}
}
#[inline]
pub fn effective_global_parallelism() -> Par {
if in_nested_parallel_region() {
Par::Seq
} else {
get_global_parallelism()
}
}
static FAER_SEQ_STATE: std::sync::Mutex<FaerSeqState> = std::sync::Mutex::new(FaerSeqState {
depth: 0,
saved: None,
});
struct FaerSeqState {
depth: usize,
saved: Option<Par>,
}
#[must_use = "the sequential scope only holds while the guard is alive"]
pub struct FaerSequentialScope {
_private: (),
}
impl FaerSequentialScope {
pub fn enter() -> Self {
let mut state = FAER_SEQ_STATE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.depth == 0 {
state.saved = Some(get_global_parallelism());
faer::set_global_parallelism(Par::Seq);
}
state.depth += 1;
Self { _private: () }
}
}
impl Drop for FaerSequentialScope {
fn drop(&mut self) {
let mut state = FAER_SEQ_STATE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.depth -= 1;
if state.depth == 0 {
if let Some(par) = state.saved.take() {
faer::set_global_parallelism(par);
}
}
}
}
pub fn faer_sequential_scope_depth() -> usize {
FAER_SEQ_STATE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.depth
}
#[inline]
pub fn with_faer_sequential<T>(body: impl FnOnce() -> T) -> T {
let faer_seq_guard = FaerSequentialScope::enter();
let out = body();
drop(faer_seq_guard);
out
}
#[derive(Debug, Error)]
pub enum FaerLinalgError {
#[error("Factorization failed in {context}")]
FactorizationFailed { context: &'static str },
#[error("SVD failed to converge in {context}")]
SvdNoConvergence { context: &'static str },
#[error("Self-adjoint eigendecomposition input contains non-finite values in {context}")]
SelfAdjointEigenNonFiniteInput { context: &'static str },
#[error("Strict self-adjoint eigendecomposition rejected its input: {reason}")]
StrictSelfAdjointEigenInvalidInput { reason: String },
#[error("Self-adjoint eigendecomposition failed: {0:?}")]
SelfAdjointEigen(solvers::EvdError),
#[error("Cholesky factorization failed: {0:?}")]
Cholesky(solvers::LltError),
#[error("LDLT factorization failed: {0:?}")]
Ldlt(solvers::LdltError),
}
pub enum FaerSymmetricFactor {
Llt(FaerLlt<f64>),
Ldlt(FaerLdlt<f64>),
Lblt(FaerLblt<f64>),
}
#[inline]
pub fn cholesky_factor_logdet(factor: MatRef<'_, f64>) -> f64 {
2.0 * diagonal_log_sum(factor.diagonal())
}
#[inline]
fn diagonal_log_sum(diagonal: DiagRef<'_, f64>) -> f64 {
diagonal
.column_vector()
.iter()
.map(|&x| x.ln())
.sum::<f64>()
}
impl FaerSymmetricFactor {
#[inline]
pub fn n(&self) -> usize {
use faer::linalg::solvers::ShapeCore;
match self {
FaerSymmetricFactor::Llt(f) => f.nrows(),
FaerSymmetricFactor::Ldlt(f) => f.nrows(),
FaerSymmetricFactor::Lblt(f) => f.nrows(),
}
}
#[inline]
pub fn solve(&self, rhs: MatRef<'_, f64>) -> Mat<f64> {
match self {
FaerSymmetricFactor::Llt(f) => f.solve(rhs),
FaerSymmetricFactor::Ldlt(f) => f.solve(rhs),
FaerSymmetricFactor::Lblt(f) => f.solve(rhs),
}
}
#[inline]
pub fn solve_in_place(&self, rhs: MatMut<'_, f64>) {
match self {
FaerSymmetricFactor::Llt(f) => f.solve_in_place(rhs),
FaerSymmetricFactor::Ldlt(f) => f.solve_in_place(rhs),
FaerSymmetricFactor::Lblt(f) => f.solve_in_place(rhs),
}
}
}
impl crate::matrix::FactorizedSystem for FaerSymmetricFactor {
fn solve(&self, rhs: &Array1<f64>) -> Result<Array1<f64>, String> {
let mut out = rhs.clone();
let mut out_mat = array1_to_col_matmut(&mut out);
self.solve_in_place(out_mat.as_mut());
if !out.iter().all(|v| v.is_finite()) {
return Err("symmetric factor solve produced non-finite values".to_string());
}
Ok(out)
}
fn solvemulti(&self, rhs: &Array2<f64>) -> Result<Array2<f64>, String> {
let mut out = Array2::<f64>::zeros(rhs.raw_dim());
for j in 0..rhs.ncols() {
for i in 0..rhs.nrows() {
out[[i, j]] = rhs[[i, j]];
}
}
let mut out_mat = array2_to_matmut(&mut out);
self.solve_in_place(out_mat.as_mut());
if !out.iter().all(|v| v.is_finite()) {
return Err("symmetric factor multi-solve produced non-finite values".to_string());
}
Ok(out)
}
fn logdet(&self) -> f64 {
match self {
FaerSymmetricFactor::Llt(f) => cholesky_factor_logdet(f.L()),
FaerSymmetricFactor::Ldlt(f) => diagonal_log_sum(f.D()),
FaerSymmetricFactor::Lblt(..) => {
f64::NAN
}
}
}
}
#[inline]
pub fn factorize_symmetricwith_fallback(
matrix: MatRef<'_, f64>,
side: Side,
) -> Result<FaerSymmetricFactor, FaerLinalgError> {
if let Ok(llt) = FaerLlt::new(matrix, side) {
return Ok(FaerSymmetricFactor::Llt(llt));
}
let ldlt_err = match FaerLdlt::new(matrix, side) {
Ok(ldlt) => return Ok(FaerSymmetricFactor::Ldlt(ldlt)),
Err(err) => err,
};
let lblt = catch_unwind(AssertUnwindSafe(|| FaerLblt::new(matrix, side)))
.map_err(|_| FaerLinalgError::Ldlt(ldlt_err))?;
Ok(FaerSymmetricFactor::Lblt(lblt))
}
#[inline]
const fn should_use_faer_matmul(m: usize, n: usize, k: usize) -> bool {
const MIN_DIM: usize = 32;
const MIN_FLOP_SCALE: usize = 64 * 64;
(m >= MIN_DIM || n >= MIN_DIM || k >= MIN_DIM)
&& m.saturating_mul(n).saturating_mul(k) >= MIN_FLOP_SCALE
}
#[inline]
pub fn matmul_parallelism(m: usize, n: usize, k: usize) -> Par {
const PAR_MIN_FLOP_SCALE: usize = 2_000_000;
const PAR_MIN_LONG_DIM: usize = 256;
let flop_scale = m.saturating_mul(n).saturating_mul(k);
let long_dim = m.max(n).max(k);
if flop_scale >= PAR_MIN_FLOP_SCALE && long_dim >= PAR_MIN_LONG_DIM {
effective_global_parallelism()
} else {
Par::Seq
}
}
#[inline]
pub fn array2_to_matmut(array: &mut Array2<f64>) -> MatMut<'_, f64> {
let (rows, cols) = array.dim();
let strides = array.strides();
let s0 = strides[0];
let s1 = strides[1];
unsafe { MatMut::from_raw_parts_mut(array.as_mut_ptr(), rows, cols, s0, s1) }
}
pub fn array2_to_nested_vec(array: &Array2<f64>) -> Vec<Vec<f64>> {
array.rows().into_iter().map(|row| row.to_vec()).collect()
}
#[inline]
pub fn array1_to_col_matmut(array: &mut Array1<f64>) -> MatMut<'_, f64> {
let len = array.len();
let stride = array.strides()[0];
unsafe {
MatMut::from_raw_parts_mut(
array.as_mut_ptr(),
len,
1,
stride,
0, )
}
}
#[inline]
pub fn fast_ata<S: Data<Elem = f64>>(a: &ArrayBase<S, Ix2>) -> Array2<f64> {
let p = a.ncols();
let mut out = Array2::<f64>::zeros((p, p));
fast_ata_into(a, &mut out);
out
}
#[inline]
pub fn fast_ata_into<S: Data<Elem = f64>>(a: &ArrayBase<S, Ix2>, out: &mut Array2<f64>) {
use faer::Accum;
use faer::linalg::matmul::triangular::{BlockStructure, matmul as tri_matmul};
let (n, p) = a.dim();
assert_eq!(out.nrows(), p, "output rows must match p");
assert_eq!(out.ncols(), p, "output cols must match p");
if !should_use_faer_matmul(p, p, n) {
out.assign(&a.t().dot(a));
return;
}
let mut outview = array2_to_matmut(out);
let aview = FaerArrayView::new(a);
let a_ref = aview.as_ref();
let a_t = a_ref.transpose();
let par = matmul_parallelism(p, p, n);
tri_matmul(
outview.as_mut(),
BlockStructure::TriangularLower,
Accum::Replace,
a_t,
BlockStructure::Rectangular,
a_ref,
BlockStructure::Rectangular,
1.0,
par,
);
for i in 0..p {
for j in (i + 1)..p {
out[[i, j]] = out[[j, i]];
}
}
}
#[inline]
pub fn fast_atb<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
) -> Array2<f64> {
if let Some(out) =
crate::gpu_hook::gpu_dispatch().and_then(|d| d.try_fast_atb(a.view(), b.view()))
{
return out;
}
let (n_a, p) = a.dim();
let q = b.ncols();
fast_atb_with_parallelism(a, b, matmul_parallelism(p, q, n_a))
}
#[inline]
pub fn fast_atb_with_parallelism<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
par: Par,
) -> Array2<f64> {
use faer::linalg::matmul::matmul;
use faer::{Accum, Mat};
let (n_a, p) = a.dim();
let (n_b, q) = b.dim();
assert_eq!(n_a, n_b, "A and B must have same number of rows");
if !should_use_faer_matmul(p, q, n_a) {
return a.t().dot(b);
}
let mut result = Mat::<f64>::zeros(p, q);
let aview = FaerArrayView::new(a);
let bview = FaerArrayView::new(b);
let a_ref = aview.as_ref();
let b_ref = bview.as_ref();
matmul(
result.as_mut(),
Accum::Replace,
a_ref.transpose(),
b_ref,
1.0,
par,
);
mat_to_array(result.as_ref())
}
#[inline]
pub fn fast_abt<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
) -> Array2<f64> {
use faer::linalg::matmul::matmul;
use faer::{Accum, Mat};
let (m, k_a) = a.dim();
let (n, k_b) = b.dim();
assert_eq!(
k_a, k_b,
"A and B must have same number of columns for A·Bᵀ"
);
if !should_use_faer_matmul(m, n, k_a) {
return a.dot(&b.t());
}
let mut result = Mat::<f64>::zeros(m, n);
let aview = FaerArrayView::new(a);
let bview = FaerArrayView::new(b);
let par = matmul_parallelism(m, n, k_a);
matmul(
result.as_mut(),
Accum::Replace,
aview.as_ref(),
bview.as_ref().transpose(),
1.0,
par,
);
mat_to_array(result.as_ref())
}
#[inline]
pub fn fast_ab<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
) -> Array2<f64> {
if let Some(out) =
crate::gpu_hook::gpu_dispatch().and_then(|d| d.try_fast_ab(a.view(), b.view()))
{
return out;
}
let n = a.nrows();
let q = b.ncols();
let mut out = Array2::<f64>::zeros((n, q));
fast_ab_into(a, b, &mut out);
out
}
const FMA_LANES: usize = 8;
const KERNEL_PAR_MIN_FLOP: usize = 1 << 18;
const AV_PAR_MAX_CHUNK_ROWS: usize = 1024;
const ATV_BLOCK_ROWS: usize = 512;
#[inline]
fn kernel_should_parallelize(n: usize, p: usize) -> bool {
!in_nested_parallel_region()
&& n.saturating_mul(p) >= KERNEL_PAR_MIN_FLOP
&& rayon::current_num_threads() > 1
}
#[inline]
fn av_parallel_chunk_rows(p: usize) -> usize {
KERNEL_PAR_MIN_FLOP
.div_ceil(p.max(1))
.clamp(64, AV_PAR_MAX_CHUNK_ROWS)
}
#[inline(always)]
fn fma_dot_body(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(a.len(), b.len(), "fma_dot: operand length mismatch");
let mut sum = [0.0f64; FMA_LANES];
let mut comp = [0.0f64; FMA_LANES];
let mut ca = a.chunks_exact(FMA_LANES);
let mut cb = b.chunks_exact(FMA_LANES);
for (xa, xb) in ca.by_ref().zip(cb.by_ref()) {
for l in 0..FMA_LANES {
let x = xa[l];
let y = xb[l];
let p = x * y;
let ep = x.mul_add(y, -p);
let s = sum[l] + p;
let bb = s - sum[l];
let es = (sum[l] - (s - bb)) + (p - bb);
sum[l] = s;
comp[l] += ep + es;
}
}
let mut sr = 0.0f64;
let mut cr = 0.0f64;
for (&x, &y) in ca.remainder().iter().zip(cb.remainder().iter()) {
let p = x * y;
let ep = x.mul_add(y, -p);
let s = sr + p;
let bb = s - sr;
let es = (sr - (s - bb)) + (p - bb);
sr = s;
cr += ep + es;
}
let mut total = sr + cr;
for l in 0..FMA_LANES {
total += sum[l] + comp[l];
}
total
}
#[cfg(target_arch = "x86_64")]
#[inline]
fn fma_avx2_available() -> bool {
std::arch::is_x86_feature_detected!("fma") && std::arch::is_x86_feature_detected!("avx2")
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "fma,avx2")]
fn fma_dot_fma_avx2(a: &[f64], b: &[f64]) -> f64 {
fma_dot_body(a, b)
}
#[inline]
fn fma_dot(a: &[f64], b: &[f64]) -> f64 {
#[cfg(target_arch = "x86_64")]
if fma_avx2_available() {
return unsafe { fma_dot_fma_avx2(a, b) };
}
fma_dot_body(a, b)
}
fn fast_av_rowmajor_into(x_all: &[f64], v: &[f64], n: usize, p: usize, out: &mut [f64]) {
assert_eq!(x_all.len(), n * p, "fast_av_rowmajor_into: x_all length");
assert_eq!(v.len(), p, "fast_av_rowmajor_into: v length");
assert_eq!(out.len(), n, "fast_av_rowmajor_into: out length");
if kernel_should_parallelize(n, p) {
use rayon::prelude::*;
let chunk_rows = av_parallel_chunk_rows(p);
out.par_chunks_mut(chunk_rows)
.enumerate()
.for_each(|(c, chunk)| {
let base = c * chunk_rows;
for (k, o) in chunk.iter_mut().enumerate() {
let i = base + k;
*o = fma_dot(&x_all[i * p..i * p + p], v);
}
});
} else {
for (i, o) in out.iter_mut().enumerate() {
*o = fma_dot(&x_all[i * p..i * p + p], v);
}
}
}
#[inline(always)]
fn standard_fma_dot_body(a: &[f64], b: &[f64]) -> f64 {
assert_eq!(
a.len(),
b.len(),
"standard_fma_dot: operand length mismatch"
);
let mut sum = [0.0_f64; FMA_LANES];
let mut ca = a.chunks_exact(FMA_LANES);
let mut cb = b.chunks_exact(FMA_LANES);
for (xa, xb) in ca.by_ref().zip(cb.by_ref()) {
for lane in 0..FMA_LANES {
sum[lane] = xa[lane].mul_add(xb[lane], sum[lane]);
}
}
let mut remainder = 0.0;
for (&x, &y) in ca.remainder().iter().zip(cb.remainder().iter()) {
remainder = x.mul_add(y, remainder);
}
let pair01 = sum[0] + sum[1];
let pair23 = sum[2] + sum[3];
let pair45 = sum[4] + sum[5];
let pair67 = sum[6] + sum[7];
remainder + (pair01 + pair23) + (pair45 + pair67)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "fma,avx2")]
fn standard_fma_dot_fma_avx2(a: &[f64], b: &[f64]) -> f64 {
standard_fma_dot_body(a, b)
}
#[inline]
fn standard_fma_dot(a: &[f64], b: &[f64]) -> f64 {
#[cfg(target_arch = "x86_64")]
if fma_avx2_available() {
return unsafe { standard_fma_dot_fma_avx2(a, b) };
}
standard_fma_dot_body(a, b)
}
fn standard_av_rowmajor_into(x_all: &[f64], v: &[f64], n: usize, p: usize, out: &mut [f64]) {
assert_eq!(
x_all.len(),
n * p,
"standard_av_rowmajor_into: matrix length"
);
assert_eq!(v.len(), p, "standard_av_rowmajor_into: vector length");
assert_eq!(out.len(), n, "standard_av_rowmajor_into: output length");
if kernel_should_parallelize(n, p) {
use rayon::prelude::*;
let chunk_rows = av_parallel_chunk_rows(p);
out.par_chunks_mut(chunk_rows)
.enumerate()
.for_each(|(chunk_index, chunk)| {
let base = chunk_index * chunk_rows;
for (offset, output) in chunk.iter_mut().enumerate() {
let row = base + offset;
*output = standard_fma_dot(&x_all[row * p..row * p + p], v);
}
});
} else {
for (row, output) in out.iter_mut().enumerate() {
*output = standard_fma_dot(&x_all[row * p..row * p + p], v);
}
}
}
fn pairwise_sum_into(parts: &[Vec<f64>], out: &mut [f64]) {
match parts.len() {
0 => out.fill(0.0),
1 => out.copy_from_slice(&parts[0]),
_ => {
let mid = parts.len() / 2;
let p = out.len();
let mut left = vec![0.0f64; p];
let mut right = vec![0.0f64; p];
pairwise_sum_into(&parts[..mid], &mut left);
pairwise_sum_into(&parts[mid..], &mut right);
for ((o, &l), &r) in out.iter_mut().zip(left.iter()).zip(right.iter()) {
*o = l + r;
}
}
}
}
fn fast_atv_rowmajor_into(x_all: &[f64], v: &[f64], n: usize, p: usize, out: &mut [f64]) {
assert_eq!(x_all.len(), n * p, "fast_atv_rowmajor_into: x_all length");
assert_eq!(v.len(), n, "fast_atv_rowmajor_into: v length");
assert_eq!(out.len(), p, "fast_atv_rowmajor_into: out length");
let nblocks = n.div_ceil(ATV_BLOCK_ROWS);
let block_partial = |b: usize| -> Vec<f64> {
let start = b * ATV_BLOCK_ROWS;
let end = (start + ATV_BLOCK_ROWS).min(n);
let mut acc = vec![0.0f64; p];
atv_block_accumulate(&x_all[start * p..end * p], &v[start..end], &mut acc);
acc
};
let partials: Vec<Vec<f64>> = if kernel_should_parallelize(n, p) {
use rayon::prelude::*;
(0..nblocks).into_par_iter().map(block_partial).collect()
} else {
(0..nblocks).map(block_partial).collect()
};
pairwise_sum_into(&partials, out);
}
#[inline(always)]
fn atv_block_accumulate_body(rows: &[f64], v: &[f64], acc: &mut [f64]) {
let p = acc.len();
assert_eq!(rows.len(), v.len() * p, "atv_block_accumulate: block length");
for (&vi, row) in v.iter().zip(rows.chunks_exact(p)) {
for (a, &xij) in acc.iter_mut().zip(row.iter()) {
*a = xij.mul_add(vi, *a);
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "fma,avx2")]
fn atv_block_accumulate_fma_avx2(rows: &[f64], v: &[f64], acc: &mut [f64]) {
atv_block_accumulate_body(rows, v, acc)
}
#[inline]
fn atv_block_accumulate(rows: &[f64], v: &[f64], acc: &mut [f64]) {
#[cfg(target_arch = "x86_64")]
if fma_avx2_available() {
return unsafe { atv_block_accumulate_fma_avx2(rows, v, acc) };
}
atv_block_accumulate_body(rows, v, acc)
}
pub(crate) fn fma_axpy_into(alpha: f64, x: &[f64], y: &mut [f64]) {
#[cfg(target_arch = "x86_64")]
if fma_avx2_available() {
return unsafe { fma_axpy_into_fma_avx2(alpha, x, y) };
}
fma_axpy_into_body(alpha, x, y)
}
#[inline(always)]
fn fma_axpy_into_body(alpha: f64, x: &[f64], y: &mut [f64]) {
assert_eq!(x.len(), y.len(), "fma_axpy_into: operand length mismatch");
for (yi, &xi) in y.iter_mut().zip(x.iter()) {
*yi = alpha.mul_add(xi, *yi);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "fma,avx2")]
fn fma_axpy_into_fma_avx2(alpha: f64, x: &[f64], y: &mut [f64]) {
fma_axpy_into_body(alpha, x, y)
}
#[inline]
pub fn fast_av<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
) -> Array1<f64> {
if let Some(out) =
crate::gpu_hook::gpu_dispatch().and_then(|d| d.try_fast_av(a.view(), v.view()))
{
return out;
}
fast_av_impl(a, v)
}
#[inline]
fn fast_av_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
) -> Array1<f64> {
use faer::linalg::matmul::matmul;
use faer::{Accum, Mat};
let (n, p) = a.dim();
assert_eq!(p, v.len(), "A cols must match v length");
if let (Some(x_all), Some(vs)) = (a.as_slice(), v.as_slice())
&& n != 0
&& p != 0
{
let mut out = Array1::<f64>::zeros(n);
fast_av_rowmajor_into(
x_all,
vs,
n,
p,
out.as_slice_mut().expect("fresh Array1 is contiguous"),
);
return out;
}
if !should_use_faer_matmul(n, 1, p) {
return a.dot(v);
}
let mut result = Mat::<f64>::zeros(n, 1);
let aview = FaerArrayView::new(a);
let vview = FaerColView::new(v);
let a_ref = aview.as_ref();
let v_ref = vview.as_ref();
let par = matmul_parallelism(n, 1, p);
matmul(result.as_mut(), Accum::Replace, a_ref, v_ref, 1.0, par);
let mut out = Array1::<f64>::zeros(n);
for i in 0..n {
out[i] = result[(i, 0)];
}
out
}
#[inline]
pub fn fast_av_into<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
out: &mut Array1<f64>,
) {
fast_av_into_impl(a, v, out);
}
#[inline]
fn fast_av_into_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
out: &mut Array1<f64>,
) {
use faer::Accum;
use faer::linalg::matmul::matmul;
let (n, p) = a.dim();
assert_eq!(v.len(), p, "vector length must match A cols");
assert_eq!(out.len(), n, "output length must match A rows");
if let (Some(x_all), Some(vs)) = (a.as_slice(), v.as_slice())
&& n != 0
&& p != 0
&& let Some(out_s) = out.as_slice_mut()
{
fast_av_rowmajor_into(x_all, vs, n, p, out_s);
return;
}
if !should_use_faer_matmul(n, 1, p) {
out.assign(&a.dot(v));
return;
}
let mut outview = array1_to_col_matmut(out);
let aview = FaerArrayView::new(a);
let vview = FaerColView::new(v);
let a_ref = aview.as_ref();
let v_ref = vview.as_ref();
let par = matmul_parallelism(n, 1, p);
matmul(outview.as_mut(), Accum::Replace, a_ref, v_ref, 1.0, par);
}
#[inline]
pub fn fast_av_view_into<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
out: ArrayViewMut1<'_, f64>,
) {
fast_av_view_into_impl(a, v, out);
}
pub fn fast_av_standard_view_into<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
mut out: ArrayViewMut1<'_, f64>,
) {
use faer::Accum;
use faer::linalg::matmul::matmul;
let (n, p) = a.dim();
assert_eq!(v.len(), p, "vector length must match A cols");
assert_eq!(out.len(), n, "output length must match A rows");
if let (Some(x_all), Some(vs), Some(out_slice)) =
(a.as_slice(), v.as_slice(), out.as_slice_mut())
&& n != 0
&& p != 0
{
standard_av_rowmajor_into(x_all, vs, n, p, out_slice);
return;
}
if !should_use_faer_matmul(n, 1, p) {
out.assign(&a.dot(v));
return;
}
let len = out.len();
let stride = out.strides()[0];
let outview = unsafe { MatMut::from_raw_parts_mut(out.as_mut_ptr(), len, 1, stride, 0) };
let aview = FaerArrayView::new(a);
let vview = FaerColView::new(v);
matmul(
outview,
Accum::Replace,
aview.as_ref(),
vview.as_ref(),
1.0,
matmul_parallelism(n, 1, p),
);
}
#[inline]
fn fast_av_view_into_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
mut out: ArrayViewMut1<'_, f64>,
) {
use faer::Accum;
use faer::linalg::matmul::matmul;
let (n, p) = a.dim();
assert_eq!(v.len(), p, "vector length must match A cols");
assert_eq!(out.len(), n, "output length must match A rows");
if let (Some(x_all), Some(vs)) = (a.as_slice(), v.as_slice())
&& n != 0
&& p != 0
&& let Some(out_s) = out.as_slice_mut()
{
fast_av_rowmajor_into(x_all, vs, n, p, out_s);
return;
}
if !should_use_faer_matmul(n, 1, p) {
let prod = a.dot(v);
out.assign(&prod);
return;
}
let len = out.len();
let stride = out.strides()[0];
let outview = unsafe {
MatMut::from_raw_parts_mut(
out.as_mut_ptr(),
len,
1,
stride,
0, )
};
let aview = FaerArrayView::new(a);
let vview = FaerColView::new(v);
let a_ref = aview.as_ref();
let v_ref = vview.as_ref();
let par = matmul_parallelism(n, 1, p);
matmul(outview, Accum::Replace, a_ref, v_ref, 1.0, par);
}
#[inline]
pub fn fast_atv<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
) -> Array1<f64> {
if let Some(out) =
crate::gpu_hook::gpu_dispatch().and_then(|d| d.try_fast_atv(a.view(), v.view()))
{
return out;
}
fast_atv_impl(a, v)
}
#[inline]
fn fast_atv_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
) -> Array1<f64> {
use faer::Accum;
use faer::linalg::matmul::matmul;
let (n, p) = a.dim();
assert_eq!(n, v.len(), "A rows must match v length");
if let (Some(x_all), Some(vs)) = (a.as_slice(), v.as_slice())
&& n != 0
&& p != 0
{
let mut out = Array1::<f64>::zeros(p);
fast_atv_rowmajor_into(
x_all,
vs,
n,
p,
out.as_slice_mut().expect("fresh Array1 is contiguous"),
);
return out;
}
if !should_use_faer_matmul(p, 1, n) {
return a.t().dot(v);
}
let mut out = Array1::<f64>::zeros(p);
let mut outview = array1_to_col_matmut(&mut out);
let aview = FaerArrayView::new(a);
let vview = FaerColView::new(v);
let a_ref = aview.as_ref();
let v_ref = vview.as_ref();
let par = matmul_parallelism(p, 1, n);
matmul(
outview.as_mut(),
Accum::Replace,
a_ref.transpose(),
v_ref,
1.0,
par,
);
out
}
#[inline]
pub fn fast_atv_into<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
out: &mut Array1<f64>,
) {
fast_atv_into_impl(a, v, out);
}
#[inline]
fn fast_atv_into_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
v: &ArrayBase<S2, Ix1>,
out: &mut Array1<f64>,
) {
use faer::Accum;
use faer::linalg::matmul::matmul;
let (n, p) = a.dim();
assert_eq!(v.len(), n, "vector length must match A rows");
assert_eq!(out.len(), p, "output length must match A cols");
if let (Some(x_all), Some(vs)) = (a.as_slice(), v.as_slice())
&& n != 0
&& p != 0
&& let Some(out_s) = out.as_slice_mut()
{
fast_atv_rowmajor_into(x_all, vs, n, p, out_s);
return;
}
if !should_use_faer_matmul(p, 1, n) {
out.assign(&a.t().dot(v));
return;
}
let mut outview = array1_to_col_matmut(out);
let aview = FaerArrayView::new(a);
let vview = FaerColView::new(v);
let a_ref = aview.as_ref();
let v_ref = vview.as_ref();
let par = matmul_parallelism(p, 1, n);
matmul(
outview.as_mut(),
Accum::Replace,
a_ref.transpose(),
v_ref,
1.0,
par,
);
}
#[inline]
pub fn fast_xt_diag_x<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
x: &ArrayBase<S1, Ix2>,
w: &ArrayBase<S2, Ix1>,
) -> Array2<f64> {
assert_eq!(
x.nrows(),
w.len(),
"fast_xt_diag_x row/weight length mismatch"
);
if let Some(out) =
crate::gpu_hook::gpu_dispatch().and_then(|d| d.try_fast_xt_diag_x(x.view(), w.view()))
{
return out;
}
let p = x.ncols();
fast_xt_diag_x_with_parallelism(x, w, matmul_parallelism(p, p, x.nrows()))
}
#[inline]
pub fn fast_xt_diag_x_with_parallelism<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
x: &ArrayBase<S1, Ix2>,
w: &ArrayBase<S2, Ix1>,
par: Par,
) -> Array2<f64> {
assert_eq!(
x.nrows(),
w.len(),
"fast_xt_diag_x_with_parallelism row/weight length mismatch"
);
fast_xt_diag_x_with_parallelism_impl(x, w, par)
}
#[inline]
fn fast_xt_diag_x_with_parallelism_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
x: &ArrayBase<S1, Ix2>,
w: &ArrayBase<S2, Ix1>,
par: Par,
) -> Array2<f64> {
use ndarray::ShapeBuilder;
let p = x.ncols();
let mut result = Array2::<f64>::zeros((p, p).f());
stream_weighted_crossprod_into(
x,
w,
&mut result,
CrossprodStructure::SymmetricLower,
CrossprodAccum::Replace,
par,
);
result
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CrossprodStructure {
Full,
SymmetricLower,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CrossprodAccum {
Replace,
Add,
}
#[inline]
fn streaming_chunk_rows(cols: usize, n: usize) -> usize {
const TARGET_BYTES: usize = gam_runtime::resource::LIBRARY_ROW_CHUNK_TARGET_BYTES;
const MIN_ROWS: usize = 512;
const MAX_ROWS: usize = 131_072;
(TARGET_BYTES / (cols.max(1) * std::mem::size_of::<f64>()))
.clamp(MIN_ROWS, MAX_ROWS)
.min(n)
}
pub fn stream_weighted_crossprod_into<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
x: &ArrayBase<S1, Ix2>,
w: &ArrayBase<S2, Ix1>,
out: &mut Array2<f64>,
structure: CrossprodStructure,
accum: CrossprodAccum,
par: Par,
) {
use faer::Accum;
use faer::linalg::matmul::matmul;
use faer::linalg::matmul::triangular::{BlockStructure, matmul as tri_matmul};
use ndarray::s;
let (n, p) = x.dim();
assert_eq!(n, w.len(), "X rows must match W length");
assert_eq!(out.nrows(), p, "output rows must match X cols");
assert_eq!(out.ncols(), p, "output cols must match X cols");
if p == 0 {
return;
}
if n == 0 {
if accum == CrossprodAccum::Replace {
out.fill(0.0);
}
return;
}
if !should_use_faer_matmul(p, p, n) {
let w_x = Array2::from_shape_fn((n, p), |(i, j)| w[i] * x[[i, j]]);
let gram = x.t().dot(&w_x);
match accum {
CrossprodAccum::Replace => out.assign(&gram),
CrossprodAccum::Add => *out += &gram,
}
return;
}
let chunk_rows = streaming_chunk_rows(p, n);
if accum == CrossprodAccum::Replace {
out.fill(0.0);
}
let mut wx_chunk = Array2::<f64>::zeros((chunk_rows, p));
let x_is_row_major = x.is_standard_layout();
let w_slice_opt = w.as_slice();
{
let mut out_view = array2_to_matmut(out);
for start in (0..n).step_by(chunk_rows) {
let rows = (n - start).min(chunk_rows);
{
let chunk_slice = wx_chunk
.as_slice_mut()
.expect("row-major chunk is contiguous");
if x_is_row_major && let (Some(x_all), Some(w_all)) = (x.as_slice(), w_slice_opt) {
for local in 0..rows {
let src = start + local;
let wi = w_all[src];
let src_off = src * p;
let dst_off = local * p;
let src_row = &x_all[src_off..src_off + p];
let dst_row = &mut chunk_slice[dst_off..dst_off + p];
for col in 0..p {
dst_row[col] = src_row[col] * wi;
}
}
} else {
let x_slice = x.slice(s![start..start + rows, ..]);
for local in 0..rows {
let wi = w[start + local];
let xrow = x_slice.row(local);
let dst_off = local * p;
let dst_row = &mut chunk_slice[dst_off..dst_off + p];
for (col, xij) in xrow.iter().enumerate() {
dst_row[col] = xij * wi;
}
}
}
}
let x_slice = x.slice(s![start..start + rows, ..]);
let wx_slice = wx_chunk.slice(s![0..rows, ..]);
let x_view = FaerArrayView::new(&x_slice);
let wx_view = FaerArrayView::new(&wx_slice);
match structure {
CrossprodStructure::SymmetricLower => {
tri_matmul(
out_view.as_mut(),
BlockStructure::TriangularLower,
Accum::Add,
x_view.as_ref().transpose(),
BlockStructure::Rectangular,
wx_view.as_ref(),
BlockStructure::Rectangular,
1.0,
par,
);
}
CrossprodStructure::Full => {
matmul(
out_view.as_mut(),
Accum::Add,
x_view.as_ref().transpose(),
wx_view.as_ref(),
1.0,
par,
);
}
}
}
}
if structure == CrossprodStructure::SymmetricLower {
for i in 0..p {
for j in (i + 1)..p {
out[[i, j]] = out[[j, i]];
}
}
}
}
#[inline]
pub fn fast_xt_diag_y<S1: Data<Elem = f64>, S2: Data<Elem = f64>, S3: Data<Elem = f64>>(
x: &ArrayBase<S1, Ix2>,
w: &ArrayBase<S2, Ix1>,
y: &ArrayBase<S3, Ix2>,
) -> Array2<f64> {
assert_eq!(x.nrows(), y.nrows(), "fast_xt_diag_y X/Y row mismatch");
assert_eq!(
y.nrows(),
w.len(),
"fast_xt_diag_y row/weight length mismatch"
);
if let Some(out) = crate::gpu_hook::gpu_dispatch()
.and_then(|d| d.try_fast_xt_diag_y(x.view(), w.view(), y.view()))
{
return out;
}
fast_xt_diag_y_impl(x, w, y)
}
#[inline]
fn fast_xt_diag_y_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>, S3: Data<Elem = f64>>(
x: &ArrayBase<S1, Ix2>,
w: &ArrayBase<S2, Ix1>,
y: &ArrayBase<S3, Ix2>,
) -> Array2<f64> {
use faer::Accum;
use faer::linalg::matmul::matmul;
use ndarray::{ShapeBuilder, s};
let (n, q) = y.dim();
let px = x.ncols();
assert_eq!(n, w.len(), "Y rows must match W length");
assert_eq!(n, x.nrows(), "X rows must match Y rows");
if n == 0 || px == 0 || q == 0 {
return Array2::<f64>::zeros((px, q));
}
if !should_use_faer_matmul(px, q, n) {
let w_y = Array2::from_shape_fn((n, q), |(i, j)| w[i] * y[[i, j]]);
return x.t().dot(&w_y);
}
let total_cols = px + q;
let chunk_rows = streaming_chunk_rows(total_cols, n);
let mut result = Array2::<f64>::zeros((px, q).f());
let mut wy_chunk = Array2::<f64>::zeros((chunk_rows, q));
let y_is_row_major = y.is_standard_layout();
let w_slice_opt = w.as_slice();
{
let mut out_view = array2_to_matmut(&mut result);
for start in (0..n).step_by(chunk_rows) {
let rows = (n - start).min(chunk_rows);
{
let chunk_slice = wy_chunk
.as_slice_mut()
.expect("row-major chunk is contiguous");
if y_is_row_major && let (Some(y_all), Some(w_all)) = (y.as_slice(), w_slice_opt) {
for local in 0..rows {
let src = start + local;
let wi = w_all[src];
let src_off = src * q;
let dst_off = local * q;
let src_row = &y_all[src_off..src_off + q];
let dst_row = &mut chunk_slice[dst_off..dst_off + q];
for col in 0..q {
dst_row[col] = src_row[col] * wi;
}
}
} else {
let y_slice = y.slice(s![start..start + rows, ..]);
for local in 0..rows {
let wi = w[start + local];
let yrow = y_slice.row(local);
let dst_off = local * q;
let dst_row = &mut chunk_slice[dst_off..dst_off + q];
for (col, yij) in yrow.iter().enumerate() {
dst_row[col] = yij * wi;
}
}
}
}
let x_slice = x.slice(s![start..start + rows, ..]);
let wy_slice = wy_chunk.slice(s![0..rows, ..]);
let x_view = FaerArrayView::new(&x_slice);
let wy_view = FaerArrayView::new(&wy_slice);
let par = matmul_parallelism(px, q, rows);
matmul(
out_view.as_mut(),
Accum::Add,
x_view.as_ref().transpose(),
wy_view.as_ref(),
1.0,
par,
);
}
}
result
}
pub fn fast_joint_hessian_2x2<
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
S3: Data<Elem = f64>,
S4: Data<Elem = f64>,
S5: Data<Elem = f64>,
>(
x_a: &ArrayBase<S1, Ix2>,
x_b: &ArrayBase<S2, Ix2>,
w_aa: &ArrayBase<S3, Ix1>,
w_ab: &ArrayBase<S4, Ix1>,
w_bb: &ArrayBase<S5, Ix1>,
) -> Array2<f64> {
if let Some(out) = crate::gpu_hook::gpu_dispatch().and_then(|d| {
d.try_fast_joint_hessian_2x2(
x_a.view(),
x_b.view(),
w_aa.view(),
w_ab.view(),
w_bb.view(),
)
}) {
return out;
}
fast_joint_hessian_2x2_impl(x_a, x_b, w_aa, w_ab, w_bb)
}
#[inline]
fn fast_joint_hessian_2x2_impl<
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
S3: Data<Elem = f64>,
S4: Data<Elem = f64>,
S5: Data<Elem = f64>,
>(
x_a: &ArrayBase<S1, Ix2>,
x_b: &ArrayBase<S2, Ix2>,
w_aa: &ArrayBase<S3, Ix1>,
w_ab: &ArrayBase<S4, Ix1>,
w_bb: &ArrayBase<S5, Ix1>,
) -> Array2<f64> {
use faer::Accum;
use faer::linalg::matmul::matmul;
use ndarray::{ShapeBuilder, s};
let n = x_a.nrows();
let pa = x_a.ncols();
let pb = x_b.ncols();
let total = pa + pb;
assert_eq!(n, x_b.nrows());
assert_eq!(n, w_aa.len());
assert_eq!(n, w_ab.len());
assert_eq!(n, w_bb.len());
if n == 0 || total == 0 {
return Array2::<f64>::zeros((total, total));
}
if !should_use_faer_matmul(pa.max(pb), pa.max(pb), n) {
let waa_xa = Array2::from_shape_fn((n, pa), |(i, j)| w_aa[i] * x_a[[i, j]]);
let wab_xb = Array2::from_shape_fn((n, pb), |(i, j)| w_ab[i] * x_b[[i, j]]);
let wbb_xb = Array2::from_shape_fn((n, pb), |(i, j)| w_bb[i] * x_b[[i, j]]);
let mut out = Array2::<f64>::zeros((total, total));
out.slice_mut(s![..pa, ..pa]).assign(&x_a.t().dot(&waa_xa));
out.slice_mut(s![..pa, pa..]).assign(&x_a.t().dot(&wab_xb));
out.slice_mut(s![pa.., pa..]).assign(&x_b.t().dot(&wbb_xb));
for i in 0..total {
for j in 0..i {
out[[i, j]] = out[[j, i]];
}
}
return out;
}
let cols_needed = pa + 2 * pb;
let chunk_rows = streaming_chunk_rows(cols_needed, n);
let mut out = Array2::<f64>::zeros((total, total).f());
let mut waa_xa_buf = Array2::<f64>::zeros((chunk_rows, pa));
let mut wab_xb_buf = Array2::<f64>::zeros((chunk_rows, pb));
let mut wbb_xb_buf = Array2::<f64>::zeros((chunk_rows, pb));
let xa_is_row_major = x_a.is_standard_layout();
let xb_is_row_major = x_b.is_standard_layout();
let waa_slice_opt = w_aa.as_slice();
let wab_slice_opt = w_ab.as_slice();
let wbb_slice_opt = w_bb.as_slice();
{
let mut out_mat = array2_to_matmut(&mut out);
for start in (0..n).step_by(chunk_rows) {
let rows = (n - start).min(chunk_rows);
let xa_slice = x_a.slice(s![start..start + rows, ..]);
let xb_slice = x_b.slice(s![start..start + rows, ..]);
{
let waa_chunk = waa_xa_buf
.as_slice_mut()
.expect("row-major waa chunk is contiguous");
let wab_chunk = wab_xb_buf
.as_slice_mut()
.expect("row-major wab chunk is contiguous");
let wbb_chunk = wbb_xb_buf
.as_slice_mut()
.expect("row-major wbb chunk is contiguous");
if xa_is_row_major
&& xb_is_row_major
&& let (Some(xa_all), Some(xb_all)) = (x_a.as_slice(), x_b.as_slice())
&& let (Some(waa_all), Some(wab_all), Some(wbb_all)) =
(waa_slice_opt, wab_slice_opt, wbb_slice_opt)
{
for local in 0..rows {
let i = start + local;
let waa_i = waa_all[i];
let wab_i = wab_all[i];
let wbb_i = wbb_all[i];
let xa_off = i * pa;
let xa_row = &xa_all[xa_off..xa_off + pa];
let xb_off = i * pb;
let xb_row = &xb_all[xb_off..xb_off + pb];
let waa_off = local * pa;
let wab_off = local * pb;
let wbb_off = local * pb;
let waa_row = &mut waa_chunk[waa_off..waa_off + pa];
for col in 0..pa {
waa_row[col] = xa_row[col] * waa_i;
}
let wab_row = &mut wab_chunk[wab_off..wab_off + pb];
let wbb_row = &mut wbb_chunk[wbb_off..wbb_off + pb];
for col in 0..pb {
let xij = xb_row[col];
wab_row[col] = xij * wab_i;
wbb_row[col] = xij * wbb_i;
}
}
} else {
for local in 0..rows {
let i = start + local;
let waa_i = w_aa[i];
let wab_i = w_ab[i];
let wbb_i = w_bb[i];
let waa_off = local * pa;
let wab_off = local * pb;
let wbb_off = local * pb;
let waa_row = &mut waa_chunk[waa_off..waa_off + pa];
let xa_row = xa_slice.row(local);
for (col, xij) in xa_row.iter().enumerate() {
waa_row[col] = xij * waa_i;
}
let wab_row = &mut wab_chunk[wab_off..wab_off + pb];
let wbb_row = &mut wbb_chunk[wbb_off..wbb_off + pb];
let xb_row = xb_slice.row(local);
for (col, xij) in xb_row.iter().enumerate() {
wab_row[col] = xij * wab_i;
wbb_row[col] = xij * wbb_i;
}
}
}
}
let xa_view = FaerArrayView::new(&xa_slice);
let xb_view = FaerArrayView::new(&xb_slice);
let waa_xa_slice = waa_xa_buf.slice(s![0..rows, ..]);
let wab_xb_slice = wab_xb_buf.slice(s![0..rows, ..]);
let wbb_xb_slice = wbb_xb_buf.slice(s![0..rows, ..]);
let waa_xa_view = FaerArrayView::new(&waa_xa_slice);
let wab_xb_view = FaerArrayView::new(&wab_xb_slice);
let wbb_xb_view = FaerArrayView::new(&wbb_xb_slice);
matmul(
out_mat.rb_mut().submatrix_mut(0, 0, pa, pa),
Accum::Add,
xa_view.as_ref().transpose(),
waa_xa_view.as_ref(),
1.0,
matmul_parallelism(pa, pa, rows),
);
matmul(
out_mat.rb_mut().submatrix_mut(0, pa, pa, pb),
Accum::Add,
xa_view.as_ref().transpose(),
wab_xb_view.as_ref(),
1.0,
matmul_parallelism(pa, pb, rows),
);
matmul(
out_mat.rb_mut().submatrix_mut(pa, pa, pb, pb),
Accum::Add,
xb_view.as_ref().transpose(),
wbb_xb_view.as_ref(),
1.0,
matmul_parallelism(pb, pb, rows),
);
}
} for i in 0..total {
for j in 0..i {
out[[i, j]] = out[[j, i]];
}
}
out
}
fn mat_to_array(mat: MatRef<'_, f64>) -> Array2<f64> {
let nrows = mat.nrows();
let ncols = mat.ncols();
let mut out = Array2::<f64>::zeros((nrows, ncols));
if nrows == 0 || ncols == 0 {
return out;
}
if let Some(out_slice) = out.as_slice_memory_order_mut() {
for i in 0..nrows {
let row_start = i * ncols;
for j in 0..ncols {
out_slice[row_start + j] = mat[(i, j)];
}
}
} else {
for j in 0..ncols {
for i in 0..nrows {
out[[i, j]] = mat[(i, j)];
}
}
}
out
}
#[inline]
pub fn fast_ab_into<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
out: &mut Array2<f64>,
) {
fast_ab_into_impl(a, b, out);
}
#[inline]
fn fast_ab_into_impl<S1: Data<Elem = f64>, S2: Data<Elem = f64>>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
out: &mut Array2<f64>,
) {
use faer::Accum;
use faer::linalg::matmul::matmul;
let (n, p) = a.dim();
let (p_b, q) = b.dim();
assert_eq!(p, p_b, "A and B must have compatible inner dimensions");
assert_eq!(out.dim(), (n, q), "output dimensions must match A*B result");
if !should_use_faer_matmul(n, q, p) {
out.assign(&a.dot(b));
return;
}
let aview = FaerArrayView::new(a);
let bview = FaerArrayView::new(b);
let a_ref = aview.as_ref();
let b_ref = bview.as_ref();
let par = matmul_parallelism(n, q, p);
let mut outview = array2_to_matmut(out);
matmul(outview.as_mut(), Accum::Replace, a_ref, b_ref, 1.0, par);
}
fn diag_to_array(diag: DiagRef<'_, f64>) -> Array1<f64> {
let mat = diag.column_vector().as_mat();
let mut out = Array1::<f64>::zeros(mat.nrows());
for i in 0..mat.nrows() {
out[i] = mat[(i, 0)];
}
out
}
pub struct FaerArrayView<'a> {
ptr: *const f64,
rows: usize,
cols: usize,
row_stride: isize,
col_stride: isize,
owned: Option<Array2<f64>>,
marker: PhantomData<&'a f64>,
}
impl<'a> FaerArrayView<'a> {
#[inline]
pub fn new<S: Data<Elem = f64>>(array: &'a ArrayBase<S, Ix2>) -> Self {
let (rows, cols) = array.dim();
let strides = array.strides();
if strides[0] <= 0 || strides[1] <= 0 {
let owned = array.to_owned();
let owned_strides = owned.strides();
return Self {
ptr: owned.as_ptr(),
rows,
cols,
row_stride: owned_strides[0],
col_stride: owned_strides[1],
owned: Some(owned),
marker: PhantomData,
};
}
Self {
ptr: array.as_ptr(),
rows,
cols,
row_stride: strides[0],
col_stride: strides[1],
owned: None,
marker: PhantomData,
}
}
#[inline]
pub fn as_ref(&self) -> MatRef<'_, f64> {
let (ptr, rows, cols, row_stride, col_stride) = if let Some(owned) = &self.owned {
let strides = owned.strides();
(
owned.as_ptr(),
owned.nrows(),
owned.ncols(),
strides[0],
strides[1],
)
} else {
(
self.ptr,
self.rows,
self.cols,
self.row_stride,
self.col_stride,
)
};
unsafe { MatRef::from_raw_parts(ptr, rows, cols, row_stride, col_stride) }
}
}
pub struct FaerColView<'a> {
ptr: *const f64,
len: usize,
stride: isize,
owned: Option<Array1<f64>>,
marker: PhantomData<&'a f64>,
}
impl<'a> FaerColView<'a> {
#[inline]
pub fn new<S: Data<Elem = f64>>(array: &'a ArrayBase<S, Ix1>) -> Self {
let len = array.len();
let stride = array.strides()[0];
if stride <= 0 {
let owned = array.to_owned();
return Self {
ptr: owned.as_ptr(),
len,
stride: 1,
owned: Some(owned),
marker: PhantomData,
};
}
Self {
ptr: array.as_ptr(),
len,
stride,
owned: None,
marker: PhantomData,
}
}
#[inline]
pub fn as_ref(&self) -> MatRef<'_, f64> {
let (ptr, len, stride) = if let Some(owned) = &self.owned {
(owned.as_ptr(), owned.len(), 1)
} else {
(self.ptr, self.len, self.stride)
};
unsafe { MatRef::from_raw_parts(ptr, len, 1, stride, 0) }
}
}
pub trait FaerSvd {
fn svd(
&self,
compute_u: bool,
computevt: bool,
) -> Result<(Option<Array2<f64>>, Array1<f64>, Option<Array2<f64>>), FaerLinalgError>;
}
impl<S: Data<Elem = f64>> FaerSvd for ArrayBase<S, Ix2> {
fn svd(
&self,
compute_u: bool,
computevt: bool,
) -> Result<(Option<Array2<f64>>, Array1<f64>, Option<Array2<f64>>), FaerLinalgError> {
let faerview = FaerArrayView::new(self);
let faer_mat = faerview.as_ref();
if !compute_u && !computevt {
let (rows, cols) = faer_mat.shape();
let mut singular = Diag::<f64>::zeros(rows.min(cols));
let par = get_global_parallelism();
let mut mem = MemBuffer::new(svd::svd_scratch::<f64>(
rows,
cols,
ComputeSvdVectors::No,
ComputeSvdVectors::No,
par,
Default::default(),
));
let stack = MemStack::new(&mut mem);
svd::svd(
faer_mat,
singular.as_mut(),
None,
None,
par,
stack,
Default::default(),
)
.map_err(|_| FaerLinalgError::SvdNoConvergence {
context: "faer SVD singular values only",
})?;
let singularvalues = diag_to_array(singular.as_ref());
return Ok((None, singularvalues, None));
}
let (rows, cols) = faer_mat.shape();
let rank = rows.min(cols);
let compute_u_flag = if compute_u {
ComputeSvdVectors::Thin
} else {
ComputeSvdVectors::No
};
let computev_flag = if computevt {
ComputeSvdVectors::Thin
} else {
ComputeSvdVectors::No
};
let mut singular = Diag::<f64>::zeros(rows.min(cols));
let mut u_storage = compute_u.then(|| Mat::<f64>::zeros(rows, rank));
let mut v_storage = computevt.then(|| Mat::<f64>::zeros(cols, rank));
let par = get_global_parallelism();
let mut mem = MemBuffer::new(svd::svd_scratch::<f64>(
rows,
cols,
compute_u_flag,
computev_flag,
par,
Default::default(),
));
let stack = MemStack::new(&mut mem);
svd::svd(
faer_mat.as_ref(),
singular.as_mut(),
u_storage.as_mut().map(|mat| mat.as_mut()),
v_storage.as_mut().map(|mat| mat.as_mut()),
par,
stack,
Default::default(),
)
.map_err(|_| FaerLinalgError::SvdNoConvergence {
context: "faer SVD with vectors",
})?;
let singularvalues = diag_to_array(singular.as_ref());
let u_opt = u_storage.map(|mat| mat_to_array(mat.as_ref()));
let vt_opt = v_storage.map(|mat| {
let mat_ref = mat.as_ref();
let mut out = Array2::<f64>::zeros((mat_ref.ncols(), mat_ref.nrows()));
for j in 0..mat_ref.nrows() {
for i in 0..mat_ref.ncols() {
out[[i, j]] = mat_ref[(j, i)];
}
}
out
});
Ok((u_opt, singularvalues, vt_opt))
}
}
pub trait FaerEigh {
fn eigh(&self, side: Side) -> Result<(Array1<f64>, Array2<f64>), FaerLinalgError>;
}
pub fn strict_symmetric_eigh<S: Data<Elem = f64>>(
matrix: &ArrayBase<S, Ix2>,
side: Side,
) -> Result<(Array1<f64>, Array2<f64>), FaerLinalgError> {
let owned = matrix.to_owned();
if owned.nrows() == 0 || owned.nrows() != owned.ncols() {
return Err(FaerLinalgError::StrictSelfAdjointEigenInvalidInput {
reason: format!(
"expected non-empty square matrix, got {}x{}",
owned.nrows(),
owned.ncols()
),
});
}
crate::utils::validate_finite_symmetric_matrix(
&owned,
"strict self-adjoint eigendecomposition",
)
.map_err(
|error| FaerLinalgError::StrictSelfAdjointEigenInvalidInput {
reason: error.to_string(),
},
)?;
let view = FaerArrayView::new(&owned);
let eigen = catch_unwind(AssertUnwindSafe(|| view.as_ref().self_adjoint_eigen(side)))
.map_err(|_| FaerLinalgError::FactorizationFailed {
context: "strict self-adjoint eigendecomposition panic boundary",
})?
.map_err(FaerLinalgError::SelfAdjointEigen)?;
let values = diag_to_array(eigen.S());
let vectors = mat_to_array(eigen.U());
if values.iter().any(|value| !value.is_finite())
|| vectors.iter().any(|value| !value.is_finite())
{
return Err(FaerLinalgError::SelfAdjointEigenNonFiniteInput {
context: "strict self-adjoint eigendecomposition output validation",
});
}
Ok((values, vectors))
}
impl<S: Data<Elem = f64>> FaerEigh for ArrayBase<S, Ix2> {
fn eigh(&self, side: Side) -> Result<(Array1<f64>, Array2<f64>), FaerLinalgError> {
fn try_eigh(
matrix: &Array2<f64>,
side: Side,
) -> Result<(Array1<f64>, Array2<f64>), FaerLinalgError> {
let faerview = FaerArrayView::new(matrix);
let eigh_started = std::time::Instant::now();
let eigh_par = get_global_parallelism();
let eigen = catch_unwind(AssertUnwindSafe(|| {
faerview.as_ref().self_adjoint_eigen(side)
}))
.map_err(|_| FaerLinalgError::FactorizationFailed {
context: "self-adjoint eigendecomposition panic boundary",
})?
.map_err(FaerLinalgError::SelfAdjointEigen)?;
let eigh_elapsed = eigh_started.elapsed();
let eigh_calls = EIGH_CALLS.fetch_add(1, Ordering::Relaxed) + 1;
if eigh_par == Par::Seq {
EIGH_SEQ_CALLS.fetch_add(1, Ordering::Relaxed);
}
EIGH_MAX_DIM.fetch_max(matrix.nrows() as u64, Ordering::Relaxed);
let eigh_nanos_total = EIGH_NANOS
.fetch_add(eigh_elapsed.as_nanos() as u64, Ordering::Relaxed)
+ eigh_elapsed.as_nanos() as u64;
record_thread_eigh(
eigh_par == Par::Seq,
matrix.nrows() as u64,
eigh_elapsed.as_nanos() as u64,
);
log::debug!(
"[eigh] dim={} elapsed={:.3}s faer_global_parallelism={:?} \
calls_so_far={eigh_calls} cumulative={:.3}s",
matrix.nrows(),
eigh_elapsed.as_secs_f64(),
eigh_par,
eigh_nanos_total as f64 / 1e9,
);
let values = diag_to_array(eigen.S());
let vectors = mat_to_array(eigen.U());
Ok((values, vectors))
}
let owned = self.to_owned();
if owned.nrows() != owned.ncols() {
return Err(FaerLinalgError::FactorizationFailed {
context: "self-adjoint eigendecomposition non-square input",
});
}
if owned.nrows() == 0 {
return Ok((Array1::zeros(0), Array2::zeros((0, 0))));
}
if owned.iter().any(|value| !value.is_finite()) {
return Err(FaerLinalgError::SelfAdjointEigenNonFiniteInput {
context: "self-adjoint eigendecomposition input validation",
});
}
if let Ok((evals, evecs)) = try_eigh(&owned, side)
&& evals.iter().all(|value| value.is_finite())
&& evecs.iter().all(|value| value.is_finite())
{
return Ok((evals, evecs));
}
let mut repaired = owned.clone();
crate::matrix::symmetrize_in_place(&mut repaired);
let scale = repaired
.iter()
.fold(0.0_f64, |acc, &value| acc.max(value.abs()))
.max(1.0);
let scaled = repaired.mapv(|value| value / scale);
const JITTER_SCHEDULE: [f64; 6] = [0.0, 1e-12, 1e-10, 1e-8, 1e-6, 1e-4];
let jitter_schedule = JITTER_SCHEDULE;
let mut last_error = FaerLinalgError::FactorizationFailed {
context: "self-adjoint eigendecomposition repair attempts",
};
for &jitter in &jitter_schedule {
let mut candidate = scaled.clone();
if jitter > 0.0 {
let n = candidate.nrows();
for i in 0..n {
candidate[[i, i]] += jitter;
}
}
match try_eigh(&candidate, side) {
Ok((mut evals, evecs))
if evals.iter().all(|value| value.is_finite())
&& evecs.iter().all(|value| value.is_finite()) =>
{
for value in &mut evals {
*value = (*value - jitter) * scale;
}
return Ok((evals, evecs));
}
Ok((_, _)) => {
last_error = FaerLinalgError::SelfAdjointEigenNonFiniteInput {
context: "self-adjoint eigendecomposition repaired output validation",
};
}
Err(err) => {
last_error = err;
}
}
}
Err(last_error)
}
}
pub struct FaerCholeskyFactor {
factor: solvers::Llt<f64>,
}
impl FaerCholeskyFactor {
pub fn solvevec(&self, rhs: &Array1<f64>) -> Array1<f64> {
let mut rhs = rhs.to_owned();
let mut rhsview = array1_to_col_matmut(&mut rhs);
self.factor.solve_in_place(rhsview.as_mut());
rhs
}
pub fn solve_mat_in_place(&self, rhs: &mut Array2<f64>) {
let mut rhsview = array2_to_matmut(rhs);
self.factor.solve_in_place(rhsview.as_mut());
}
pub fn solve_mat_into<S: Data<Elem = f64>>(
&self,
rhs: &ArrayBase<S, Ix2>,
out: &mut Array2<f64>,
) {
if out.dim() != rhs.dim() {
*out = Array2::<f64>::zeros(rhs.dim());
}
out.assign(rhs);
self.solve_mat_in_place(out);
}
pub fn solve_mat(&self, rhs: &Array2<f64>) -> Array2<f64> {
let mut out = Array2::<f64>::zeros(rhs.dim());
self.solve_mat_into(rhs, &mut out);
out
}
pub fn diag(&self) -> Array1<f64> {
diag_to_array(self.factor.L().diagonal())
}
pub fn lower_triangular(&self) -> Array2<f64> {
mat_to_array(self.factor.L())
}
}
impl crate::matrix::FactorizedSystem for FaerCholeskyFactor {
fn solve(&self, rhs: &Array1<f64>) -> Result<Array1<f64>, String> {
let out = self.solvevec(rhs);
if out.iter().all(|value| value.is_finite()) {
Ok(out)
} else {
Err("strict Cholesky solve produced non-finite values".to_string())
}
}
fn solvemulti(&self, rhs: &Array2<f64>) -> Result<Array2<f64>, String> {
let out = self.solve_mat(rhs);
if out.iter().all(|value| value.is_finite()) {
Ok(out)
} else {
Err("strict Cholesky multi-solve produced non-finite values".to_string())
}
}
fn logdet(&self) -> f64 {
cholesky_factor_logdet(self.factor.L())
}
}
pub trait FaerCholesky {
fn cholesky(&self, side: Side) -> Result<FaerCholeskyFactor, FaerLinalgError>;
}
impl<S: Data<Elem = f64>> FaerCholesky for ArrayBase<S, Ix2> {
fn cholesky(&self, side: Side) -> Result<FaerCholeskyFactor, FaerLinalgError> {
let faerview = FaerArrayView::new(self);
let factor = faerview
.as_ref()
.llt(side)
.map_err(FaerLinalgError::Cholesky)?;
Ok(FaerCholeskyFactor { factor })
}
}
pub trait FaerQr {
fn qr(&self) -> Result<(Array2<f64>, Array2<f64>), FaerLinalgError>;
}
impl<S: Data<Elem = f64>> FaerQr for ArrayBase<S, Ix2> {
fn qr(&self) -> Result<(Array2<f64>, Array2<f64>), FaerLinalgError> {
let faerview = FaerArrayView::new(self);
let qr = faerview.as_ref().qr();
let q = qr.compute_thin_Q();
let r = qr.thin_R();
Ok((mat_to_array(q.as_ref()), mat_to_array(r)))
}
}
pub fn rrqr_nullspace_basis<S: Data<Elem = f64>>(
a: &ArrayBase<S, Ix2>,
rank_alpha: f64,
) -> Result<(Array2<f64>, usize), FaerLinalgError> {
rrqr_nullspace_basis_inner(a, RrqrRankCutoff::RelativeAlpha(rank_alpha))
}
#[derive(Debug, Clone, Copy)]
enum RrqrRankCutoff {
RelativeAlpha(f64),
Absolute(f64),
}
pub fn rrqr_nullspace_basis_with_cutoff<S: Data<Elem = f64>>(
a: &ArrayBase<S, Ix2>,
cutoff: f64,
) -> Result<(Array2<f64>, usize), FaerLinalgError> {
rrqr_nullspace_basis_inner(a, RrqrRankCutoff::Absolute(cutoff))
}
fn rrqr_nullspace_basis_inner<S: Data<Elem = f64>>(
a: &ArrayBase<S, Ix2>,
cutoff: RrqrRankCutoff,
) -> Result<(Array2<f64>, usize), FaerLinalgError> {
let faerview = FaerArrayView::new(a);
let qr = faerview.as_ref().col_piv_qr();
let r = qr.thin_R();
let diag_len = r.nrows().min(r.ncols());
let leading_diag = if diag_len > 0 { r[(0, 0)].abs() } else { 0.0 };
let tol = match cutoff {
RrqrRankCutoff::RelativeAlpha(rank_alpha) => {
rank_alpha
* f64::EPSILON
* (a.nrows().max(a.ncols()).max(1) as f64)
* leading_diag.max(1.0)
}
RrqrRankCutoff::Absolute(tol) => tol,
};
let rank = (0..diag_len).filter(|&i| r[(i, i)].abs() > tol).count();
let z = if rank >= a.nrows() {
Array2::<f64>::zeros((a.nrows(), 0))
} else if rank == 0 {
Array2::<f64>::eye(a.nrows())
} else {
let nullity = a.nrows() - rank;
let mut selector = Mat::<f64>::zeros(a.nrows(), nullity);
for j in 0..nullity {
selector[(rank + j, j)] = 1.0;
}
let par = get_global_parallelism();
faer::linalg::householder::apply_block_householder_sequence_on_the_left_in_place_with_conj(
qr.Q_basis(),
qr.Q_coeff(),
Conj::No,
selector.as_mut(),
par,
MemStack::new(&mut MemBuffer::new(
faer::linalg::householder::apply_block_householder_sequence_on_the_left_in_place_scratch::<f64>(
a.nrows(),
qr.Q_coeff().nrows(),
nullity,
),
)),
);
mat_to_array(selector.as_ref())
};
Ok((z, rank))
}
#[inline]
pub const fn default_rrqr_rank_alpha() -> f64 {
RRQR_RANK_ALPHA
}
pub struct RrqrWithPermutation {
pub rank: usize,
pub column_permutation: Vec<usize>,
pub leading_diag_abs: f64,
pub rank_tol: f64,
}
pub fn rrqr_with_permutation<S: Data<Elem = f64>>(
a: &ArrayBase<S, Ix2>,
rank_alpha: f64,
) -> Result<RrqrWithPermutation, FaerLinalgError> {
if a.nrows() == 0 {
return Err(FaerLinalgError::FactorizationFailed {
context: "rrqr_with_permutation: input has zero rows",
});
}
let faerview = FaerArrayView::new(a);
let qr = faerview.as_ref().col_piv_qr();
let r = qr.thin_R();
let diag_len = r.nrows().min(r.ncols());
let leading_diag = if diag_len > 0 { r[(0, 0)].abs() } else { 0.0 };
let tol = rank_alpha
* f64::EPSILON
* (a.nrows().max(a.ncols()).max(1) as f64)
* leading_diag.max(1.0);
let rank = (0..diag_len).filter(|&i| r[(i, i)].abs() > tol).count();
let (forward, _inverse) = qr.P().arrays();
let column_permutation: Vec<usize> = forward.iter().copied().map(|idx| idx.unbound()).collect();
Ok(RrqrWithPermutation {
rank,
column_permutation,
leading_diag_abs: leading_diag,
rank_tol: tol,
})
}
pub struct RrqrFromGram {
pub rank: usize,
pub column_permutation: Vec<usize>,
pub rank_tol: f64,
pub leading_diag_abs: f64,
pub verdict_margin: f64,
}
pub fn rrqr_from_gram_with_permutation<S: Data<Elem = f64>>(
gram: &ArrayBase<S, Ix2>,
m_rows: usize,
rank_alpha: f64,
) -> Result<RrqrFromGram, FaerLinalgError> {
let p = gram.ncols();
if p == 0 {
return Ok(RrqrFromGram {
rank: 0,
column_permutation: Vec::new(),
rank_tol: 0.0,
leading_diag_abs: 0.0,
verdict_margin: 0.0,
});
}
if gram.nrows() != p {
return Err(FaerLinalgError::FactorizationFailed {
context: "rrqr_from_gram_with_permutation: Gram is not square",
});
}
let (evals, evecs) = gram.eigh(Side::Lower)?;
let mut f = Array2::<f64>::zeros((p, p));
for k in 0..p {
let scale = evals[k].max(0.0).sqrt();
if scale == 0.0 {
continue;
}
for i in 0..p {
f[[k, i]] = scale * evecs[[i, k]];
}
}
let faer_f = FaerArrayView::new(&f);
let qr = faer_f.as_ref().col_piv_qr();
let r = qr.thin_R();
let diag_len = r.nrows().min(r.ncols());
let pivots: Vec<f64> = (0..diag_len).map(|i| r[(i, i)].abs()).collect();
let leading_diag = pivots.first().copied().unwrap_or(0.0);
let (forward, _inverse) = qr.P().arrays();
let column_permutation: Vec<usize> = forward.iter().copied().map(|idx| idx.unbound()).collect();
let tol = rank_alpha * f64::EPSILON * (m_rows.max(p).max(1) as f64) * leading_diag.max(1.0);
let rank = pivots.iter().filter(|&&v| v > tol).count();
let min_kept = pivots[..rank].iter().copied().fold(f64::INFINITY, f64::min);
let max_dropped = pivots[rank..].iter().copied().fold(0.0f64, f64::max);
let kept_margin = if rank == 0 {
f64::INFINITY
} else {
min_kept / tol
};
let dropped_margin = if rank == diag_len {
f64::INFINITY
} else {
tol / max_dropped.max(f64::MIN_POSITIVE)
};
let gram_precision_floor = f64::EPSILON.sqrt() * leading_diag.max(1.0);
let kept_floor_margin = if rank == 0 {
f64::INFINITY
} else {
min_kept / gram_precision_floor.max(f64::MIN_POSITIVE)
};
let verdict_margin = kept_margin.min(dropped_margin).min(kept_floor_margin);
Ok(RrqrFromGram {
rank,
column_permutation,
rank_tol: tol,
leading_diag_abs: leading_diag,
verdict_margin,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{array, s};
const JOINT_GRAM_RRQR_TRUST_MARGIN_FOR_TEST: f64 = 1.0e3;
#[test]
fn rrqr_nullspace_basis_is_orthonormal_and_annihilates_transpose() {
let a = array![[1.0, 0.0], [1.0, 0.0], [0.0, 2.0], [0.0, 0.0],];
let (z, rank) =
rrqr_nullspace_basis(&a, default_rrqr_rank_alpha()).expect("RRQR should succeed");
assert_eq!(rank, 2);
assert_eq!(z.nrows(), 4);
assert_eq!(z.ncols(), 2);
let gram = z.t().dot(&z);
let ident = Array2::<f64>::eye(z.ncols());
let gram_err = (&gram - &ident)
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(gram_err < 1e-10, "Z is not orthonormal: {gram_err:e}");
let residual = a.t().dot(&z);
let resid_max = residual.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(resid_max < 1e-10, "A^T Z residual too large: {resid_max:e}");
}
#[test]
fn rrqr_with_permutation_attributes_redundant_column() {
let a = array![
[1.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[0.0, 2.0, 0.0],
[0.0, 0.0, 0.0],
];
let result =
rrqr_with_permutation(&a, default_rrqr_rank_alpha()).expect("RRQR should succeed");
assert_eq!(result.rank, 2);
assert_eq!(result.column_permutation.len(), 3);
let demoted = result.column_permutation[result.rank..].to_vec();
assert!(
demoted.contains(&2) || demoted.contains(&0),
"demoted suffix should include one of the aliased columns (0 or 2), got {demoted:?}"
);
let mut sorted = result.column_permutation.clone();
sorted.sort();
assert_eq!(
sorted,
vec![0, 1, 2],
"permutation must be a valid bijection on 0..n"
);
}
#[test]
fn rrqr_with_permutation_pivots_the_larger_norm_column_first() {
let a = array![[1.0, 0.0], [0.0, 2.0], [0.0, 0.0]];
let result =
rrqr_with_permutation(&a, default_rrqr_rank_alpha()).expect("RRQR should succeed");
assert_eq!(result.rank, 2);
let perm = result.column_permutation.clone();
let mut sorted = perm.clone();
sorted.sort();
assert_eq!(
sorted,
vec![0, 1],
"permutation must be a bijection on 0..n, got {perm:?}"
);
assert_eq!(
perm,
vec![1, 0],
"column-pivoted QR must take the larger-norm column (1, norm 2) \
before the smaller (0, norm 1), got {perm:?}"
);
let norms: Vec<f64> = perm
.iter()
.map(|&j| a.column(j).iter().map(|value| value * value).sum::<f64>().sqrt())
.collect();
for window in norms.windows(2) {
assert!(
window[0] >= window[1],
"pivoted column norms must be non-increasing, got {norms:?}"
);
}
}
#[test]
fn rrqr_with_permutation_rejects_zero_rows() {
let a = Array2::<f64>::zeros((0, 3));
assert!(rrqr_with_permutation(&a, default_rrqr_rank_alpha()).is_err());
}
#[test]
fn rrqr_nullspace_basis_square_zero_matrix_is_finite_identity() {
let a = Array2::<f64>::zeros((3, 3));
let (z, rank) =
rrqr_nullspace_basis(&a, default_rrqr_rank_alpha()).expect("RRQR should succeed");
assert_eq!(rank, 0);
assert_eq!(z.dim(), (3, 3));
assert!(
z.iter().all(|v| v.is_finite()),
"square zero matrix produced a non-finite null basis: {z:?}"
);
let gram = z.t().dot(&z);
let ident = Array2::<f64>::eye(3);
let gram_err = (&gram - &ident)
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(gram_err < 1e-10, "Z is not orthonormal: {gram_err:e}");
}
#[test]
fn rrqr_nullspace_basis_detectszero_rank_matrix() {
let a = Array2::<f64>::zeros((5, 2));
let (z, rank) =
rrqr_nullspace_basis(&a, default_rrqr_rank_alpha()).expect("RRQR should succeed");
assert_eq!(rank, 0);
assert_eq!(z.dim(), (5, 5));
let ident = Array2::<f64>::eye(5);
let max_err = (&z.slice(s![.., ..5]).to_owned() - &ident)
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(max_err < 1e-10, "zero matrix should yield identity basis");
}
#[test]
fn eigh_on_nan_matrix_rejects_non_finite_input() {
let mat = array![
[1.0, 0.0, 0.0, 0.0],
[0.0, 2.0, 0.0, 0.0],
[0.0, 0.0, 3.0, f64::NAN],
[0.0, 0.0, f64::NAN, 4.0]
];
let err = mat
.eigh(Side::Lower)
.expect_err("non-finite symmetric input must be rejected");
assert!(matches!(
err,
FaerLinalgError::SelfAdjointEigenNonFiniteInput { .. }
));
}
#[test]
fn fast_ata_matches_full_gemm_above_threshold() {
let n = 200;
let p = 40;
let a: Array2<f64> = Array2::from_shape_fn((n, p), |(i, j)| {
((i * 7 + j * 3) as f64).sin() + 0.1 * j as f64
});
let expected = a.t().dot(&a);
let got = fast_ata(&a);
let max_err = (&got - &expected)
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(max_err < 1e-10, "fast_ata mismatch: {max_err:e}");
for i in 0..p {
for j in 0..p {
assert!((got[[i, j]] - got[[j, i]]).abs() < 1e-12);
}
}
}
#[test]
fn fast_xt_diag_x_matches_naive_above_threshold() {
let n = 400;
let p = 36;
let x: Array2<f64> =
Array2::from_shape_fn((n, p), |(i, j)| (i as f64 * 0.1).cos() + j as f64 * 0.05);
let w: Array1<f64> = Array1::from_shape_fn(n, |i| (i as f64 * 0.03).sin());
let wx = Array2::from_shape_fn((n, p), |(i, j)| w[i] * x[[i, j]]);
let expected = x.t().dot(&wx);
let got = fast_xt_diag_x(&x, &w);
let max_err = (&got - &expected)
.iter()
.fold(0.0_f64, |acc, &v| acc.max(v.abs()));
assert!(max_err < 1e-9, "fast_xt_diag_x mismatch: {max_err:e}");
for i in 0..p {
for j in 0..p {
assert!((got[[i, j]] - got[[j, i]]).abs() < 1e-12);
}
}
}
#[test]
fn stream_weighted_crossprod_full_and_triangular_parity_with_negative_weights() {
for &(n, p) in &[(900usize, 40usize), (8usize, 3usize)] {
let x: Array2<f64> =
Array2::from_shape_fn((n, p), |(i, j)| (i as f64 * 0.07).cos() + j as f64 * 0.013);
let w: Array1<f64> =
Array1::from_shape_fn(n, |i| (i as f64 * 0.11).sin() - 0.25 * (i % 3) as f64);
assert!(
w.iter().any(|&v| v < 0.0),
"weight vector must contain negatives to test sign preservation"
);
let wx = Array2::from_shape_fn((n, p), |(i, j)| w[i] * x[[i, j]]);
let expected = x.t().dot(&wx);
let par = matmul_parallelism(p, p, n);
let mut full = Array2::<f64>::ones((p, p));
stream_weighted_crossprod_into(
&x,
&w,
&mut full,
CrossprodStructure::Full,
CrossprodAccum::Replace,
par,
);
let mut tri = Array2::<f64>::from_elem((p, p), -7.0);
stream_weighted_crossprod_into(
&x,
&w,
&mut tri,
CrossprodStructure::SymmetricLower,
CrossprodAccum::Replace,
par,
);
let full_err = (&full - &expected)
.iter()
.fold(0.0_f64, |a, &v| a.max(v.abs()));
let tri_err = (&tri - &expected)
.iter()
.fold(0.0_f64, |a, &v| a.max(v.abs()));
assert!(
full_err < 1e-9,
"full kernel mismatch (n={n}, p={p}): {full_err:e}"
);
assert!(
tri_err < 1e-9,
"triangular kernel mismatch (n={n}, p={p}): {tri_err:e}"
);
for i in 0..p {
for j in 0..p {
assert!(
(full[[i, j]] - tri[[i, j]]).abs() < 1e-12,
"full vs triangular disagree at ({i},{j})"
);
assert!(
(tri[[i, j]] - tri[[j, i]]).abs() < 1e-12,
"triangular output not symmetric at ({i},{j})"
);
}
}
let base = Array2::<f64>::from_elem((p, p), 1.5);
let mut add_full = base.clone();
stream_weighted_crossprod_into(
&x,
&w,
&mut add_full,
CrossprodStructure::Full,
CrossprodAccum::Add,
par,
);
let mut add_tri = base.clone();
stream_weighted_crossprod_into(
&x,
&w,
&mut add_tri,
CrossprodStructure::SymmetricLower,
CrossprodAccum::Add,
par,
);
let expected_add = &base + &expected;
let add_full_err = (&add_full - &expected_add)
.iter()
.fold(0.0_f64, |a, &v| a.max(v.abs()));
let add_tri_err = (&add_tri - &expected_add)
.iter()
.fold(0.0_f64, |a, &v| a.max(v.abs()));
assert!(
add_full_err < 1e-9,
"full Add mismatch (n={n}, p={p}): {add_full_err:e}"
);
assert!(
add_tri_err < 1e-9,
"triangular Add mismatch (n={n}, p={p}): {add_tri_err:e}"
);
let returned = fast_xt_diag_x(&x, &w);
let returned_err = (&returned - &full)
.iter()
.fold(0.0_f64, |a, &v| a.max(v.abs()));
assert!(
returned_err < 1e-12,
"return adapter vs stream-into adapter disagree (n={n}, p={p}): {returned_err:e}"
);
}
}
#[test]
fn eigh_succeeds_on_same_structure_without_nan() {
let mat = array![[1.0, 0.5, 0.1], [0.5, 2.0, 0.3], [0.1, 0.3, 1.5]];
let (evals, _) = mat
.eigh(Side::Lower)
.expect("eigh should succeed on a well-conditioned finite matrix");
assert!(
evals.iter().all(|&v| v.is_finite()),
"all eigenvalues should be finite"
);
}
#[test]
fn gram_rrqr_flags_low_margin_on_exact_collinearity_so_caller_falls_back() {
let n = 48usize;
let x: Vec<f64> = (0..n)
.map(|i| -1.0 + 2.0 * (i as f64) / (n as f64 - 1.0))
.collect();
let mut a = Array2::<f64>::zeros((n, 4));
for i in 0..n {
a[[i, 0]] = 1.0;
a[[i, 1]] = x[i];
a[[i, 2]] = x[i];
a[[i, 3]] = x[i] * x[i];
}
let alpha = default_rrqr_rank_alpha();
let tall = rrqr_with_permutation(&a, alpha).expect("tall RRQR should succeed");
assert_eq!(tall.rank, 3, "tall RRQR must demote the exact alias");
let unit = Array1::<f64>::ones(n);
let gram = fast_xt_diag_x_with_parallelism(&a, &unit, faer::get_global_parallelism());
let gram_rrqr =
rrqr_from_gram_with_permutation(&gram, n, alpha).expect("Gram RRQR should succeed");
let ok =
gram_rrqr.rank == 3 || gram_rrqr.verdict_margin < JOINT_GRAM_RRQR_TRUST_MARGIN_FOR_TEST;
assert!(
ok,
"gam#933: Gram RRQR must either find correct rank=3 OR signal low margin \
(< {:.0e}) to force the tall fallback; got rank={} margin={:.3e}",
JOINT_GRAM_RRQR_TRUST_MARGIN_FOR_TEST, gram_rrqr.rank, gram_rrqr.verdict_margin,
);
}
#[test]
fn gram_rrqr_keeps_high_margin_on_full_rank_design() {
let n = 200usize;
let p = 5usize;
let mut a = Array2::<f64>::zeros((n, p));
for i in 0..n {
let t = (i as f64) / (n as f64 - 1.0);
a[[i, 0]] = 1.0;
a[[i, 1]] = t;
a[[i, 2]] = t * t;
a[[i, 3]] = t * t * t;
a[[i, 4]] = (t * 6.0).sin();
}
let alpha = default_rrqr_rank_alpha();
let unit = Array1::<f64>::ones(n);
let gram = fast_xt_diag_x_with_parallelism(&a, &unit, faer::get_global_parallelism());
let gram_rrqr =
rrqr_from_gram_with_permutation(&gram, n, alpha).expect("Gram RRQR should succeed");
assert_eq!(gram_rrqr.rank, p, "full-rank design must keep all columns");
assert!(
gram_rrqr.verdict_margin >= JOINT_GRAM_RRQR_TRUST_MARGIN_FOR_TEST,
"full-rank design must keep a high margin (fast Gram path); got {:.3e}",
gram_rrqr.verdict_margin,
);
}
fn max_abs_diff(a: &Array2<f64>, b: &Array2<f64>) -> f64 {
assert_eq!(a.dim(), b.dim(), "shape mismatch in max_abs_diff");
a.iter()
.zip(b.iter())
.fold(0.0_f64, |acc, (&x, &y)| acc.max((x - y).abs()))
}
fn max_abs_diff_1d(a: &Array1<f64>, b: &Array1<f64>) -> f64 {
assert_eq!(a.len(), b.len(), "len mismatch in max_abs_diff_1d");
a.iter()
.zip(b.iter())
.fold(0.0_f64, |acc, (&x, &y)| acc.max((x - y).abs()))
}
#[test]
fn fast_ab_small_matches_ndarray_dot() {
let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let b = array![[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]];
let got = fast_ab(&a, &b);
let want = a.dot(&b);
assert!(max_abs_diff(&got, &want) < 1e-12, "fast_ab small mismatch");
assert_eq!(got.dim(), (2, 2));
}
#[test]
fn fast_ab_large_matches_ndarray_dot() {
let n = 50usize;
let p = 40usize;
let q = 35usize;
let mut a = Array2::<f64>::zeros((n, p));
let mut b = Array2::<f64>::zeros((p, q));
let mut state = 0xDEAD_BEEF_1234_5678u64;
let next = |s: &mut u64| -> f64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
((*s >> 11) as f64 / ((1u64 << 53) as f64)) - 0.5
};
for v in a.iter_mut() {
*v = next(&mut state);
}
for v in b.iter_mut() {
*v = next(&mut state);
}
let got = fast_ab(&a, &b);
let want = a.dot(&b);
assert!(max_abs_diff(&got, &want) < 1e-9, "fast_ab large mismatch");
}
#[test]
fn fast_atb_small_matches_ndarray_dot() {
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let b = array![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0], [13.0, 14.0, 15.0]];
let got = fast_atb(&a, &b);
let want = a.t().dot(&b);
assert!(max_abs_diff(&got, &want) < 1e-12, "fast_atb small mismatch");
assert_eq!(got.dim(), (2, 3));
}
#[test]
fn fast_atb_large_matches_ndarray_dot() {
let n = 50usize;
let p = 40usize;
let q = 35usize;
let mut a = Array2::<f64>::zeros((n, p));
let mut b = Array2::<f64>::zeros((n, q));
let mut state = 0xCAFE_BABE_9876_5432u64;
let next = |s: &mut u64| -> f64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
((*s >> 11) as f64 / ((1u64 << 53) as f64)) - 0.5
};
for v in a.iter_mut() {
*v = next(&mut state);
}
for v in b.iter_mut() {
*v = next(&mut state);
}
let got = fast_atb(&a, &b);
let want = a.t().dot(&b);
assert!(max_abs_diff(&got, &want) < 1e-9, "fast_atb large mismatch");
}
#[test]
fn fast_abt_small_matches_ndarray_dot() {
let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let b = array![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0]];
let got = fast_abt(&a, &b);
let want = a.dot(&b.t());
assert!(max_abs_diff(&got, &want) < 1e-12, "fast_abt small mismatch");
assert_eq!(got.dim(), (2, 2));
}
#[test]
fn fast_av_small_matches_ndarray_dot() {
let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let v = array![1.0, -1.0, 2.0];
let got = fast_av(&a, &v);
let want = a.dot(&v);
assert!(
max_abs_diff_1d(&got, &want) < 1e-12,
"fast_av small mismatch"
);
assert!((got[0] - 5.0).abs() < 1e-12, "fast_av[0] should be 5");
assert!((got[1] - 11.0).abs() < 1e-12, "fast_av[1] should be 11");
}
#[test]
fn fast_av_large_matches_ndarray_dot() {
let n = 50usize;
let p = 40usize;
let mut a = Array2::<f64>::zeros((n, p));
let mut v = Array1::<f64>::zeros(p);
let mut state = 0xFEED_FACE_ABCD_EF01u64;
let next = |s: &mut u64| -> f64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
((*s >> 11) as f64 / ((1u64 << 53) as f64)) - 0.5
};
for v in a.iter_mut() {
*v = next(&mut state);
}
for x in v.iter_mut() {
*x = next(&mut state);
}
let got = fast_av(&a, &v);
let want = a.dot(&v);
assert!(
max_abs_diff_1d(&got, &want) < 1e-9,
"fast_av large mismatch"
);
}
#[test]
fn standard_fma_av_matches_ndarray_dot() {
let n = 73usize;
let p = 257usize;
let a = Array2::from_shape_fn((n, p), |(i, j)| {
((i + 3 * j + 1) as f64).sin() / (j + 1) as f64
});
let v = Array1::from_shape_fn(p, |j| ((2 * j + 1) as f64).cos());
let want = a.dot(&v);
let mut got = Array1::<f64>::zeros(n);
fast_av_standard_view_into(&a, &v, got.view_mut());
assert!(
max_abs_diff_1d(&got, &want) < 1e-12,
"standard-FMA matrix-vector product mismatch"
);
}
#[test]
fn fast_atv_small_matches_ndarray_dot() {
let a = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let v = array![1.0, 0.0, -1.0];
let got = fast_atv(&a, &v);
let want = a.t().dot(&v);
assert!(
max_abs_diff_1d(&got, &want) < 1e-12,
"fast_atv small mismatch"
);
assert!((got[0] - (-4.0)).abs() < 1e-12, "fast_atv[0]");
assert!((got[1] - (-4.0)).abs() < 1e-12, "fast_atv[1]");
}
#[test]
fn fast_atv_large_matches_ndarray_dot() {
let n = 50usize;
let p = 40usize;
let mut a = Array2::<f64>::zeros((n, p));
let mut v = Array1::<f64>::zeros(n);
let mut state = 0x1234_ABCD_5678_EF90u64;
let next = |s: &mut u64| -> f64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
((*s >> 11) as f64 / ((1u64 << 53) as f64)) - 0.5
};
for x in a.iter_mut() {
*x = next(&mut state);
}
for x in v.iter_mut() {
*x = next(&mut state);
}
let got = fast_atv(&a, &v);
let want = a.t().dot(&v);
assert!(
max_abs_diff_1d(&got, &want) < 1e-9,
"fast_atv large mismatch"
);
}
#[test]
fn fast_xt_diag_y_small_matches_manual() {
let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let d = array![2.0, 0.5, 1.0];
let y = array![[7.0, 8.0, 9.0], [10.0, 11.0, 12.0], [13.0, 14.0, 15.0]];
let got = fast_xt_diag_y(&x, &d, &y);
let diag_y = {
let mut dy = Array2::<f64>::zeros(y.dim());
for i in 0..3 {
for j in 0..3 {
dy[[i, j]] = d[i] * y[[i, j]];
}
}
dy
};
let want = x.t().dot(&diag_y);
assert!(
max_abs_diff(&got, &want) < 1e-12,
"fast_xt_diag_y small mismatch"
);
assert_eq!(got.dim(), (2, 3));
}
#[inline]
fn two_prod(a: f64, b: f64) -> (f64, f64) {
let p = a * b;
let e = a.mul_add(b, -p);
(p, e)
}
#[inline]
fn two_sum(a: f64, b: f64) -> (f64, f64) {
let s = a + b;
let bb = s - a;
let e = (a - (s - bb)) + (b - bb);
(s, e)
}
fn grow_expansion(e: &mut Vec<f64>, mut q: f64) {
for h in e.iter_mut() {
let (s, err) = two_sum(*h, q);
*h = err;
q = s;
}
if q != 0.0 {
e.push(q);
}
}
fn exact_dot(a: &[f64], b: &[f64]) -> f64 {
let mut e: Vec<f64> = Vec::new();
for (&x, &y) in a.iter().zip(b.iter()) {
let (p, ep) = two_prod(x, y);
grow_expansion(&mut e, p);
grow_expansion(&mut e, ep);
}
e.iter().fold(0.0f64, |acc, &c| acc + c)
}
fn dd_dot(a: &[f64], b: &[f64]) -> f64 {
let (mut s, mut c) = (0.0f64, 0.0f64);
for (&x, &y) in a.iter().zip(b.iter()) {
let (p, ep) = two_prod(x, y);
let (s2, es) = two_sum(s, p);
s = s2;
c += ep + es;
}
s + c
}
fn naive_dot(a: &[f64], b: &[f64]) -> f64 {
let mut acc = 0.0f64;
for (&x, &y) in a.iter().zip(b.iter()) {
acc += x * y;
}
acc
}
fn ill_conditioned_pair(len: usize, seed: u64) -> (Vec<f64>, Vec<f64>) {
let mut s = seed | 1;
let mut next = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 11) as f64 / ((1u64 << 53) as f64) - 0.5
};
let mut a = Vec::with_capacity(len);
let mut b = Vec::with_capacity(len);
for i in 0..len {
let scale = 10f64.powi((i % 17) as i32 - 8);
let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
a.push(sign * next() * scale);
b.push(next() * scale);
}
(a, b)
}
#[cfg(target_arch = "x86_64")]
#[test]
fn fma_avx2_kernel_variants_are_bit_identical_to_the_baseline_bodies() {
assert!(
super::fma_avx2_available(),
"this machine reports no fma/avx2: the variant path cannot be exercised here"
);
for seed in 0..64u64 {
let len = 200 + (seed as usize % 57);
let (a, b) = ill_conditioned_pair(len, 0x9E37_79B9 ^ seed.wrapping_mul(2654435761));
let (dot_v, std_v) = unsafe {
(
super::fma_dot_fma_avx2(&a, &b),
super::standard_fma_dot_fma_avx2(&a, &b),
)
};
assert_eq!(dot_v.to_bits(), super::fma_dot_body(&a, &b).to_bits(), "fma_dot seed={seed}");
assert_eq!(
std_v.to_bits(),
super::standard_fma_dot_body(&a, &b).to_bits(),
"standard_fma_dot seed={seed}"
);
let p = 7;
let rows: Vec<f64> = (0..len * p).map(|k| a[k % len] * (1.0 + (k % 3) as f64)).collect();
let mut acc_body = vec![0.0f64; p];
let mut acc_var = vec![0.0f64; p];
super::atv_block_accumulate_body(&rows, &b, &mut acc_body);
unsafe { super::atv_block_accumulate_fma_avx2(&rows, &b, &mut acc_var) };
assert_eq!(
acc_var.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
acc_body.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"atv block seed={seed}"
);
let mut y_body = b.clone();
let mut y_var = b.clone();
super::fma_axpy_into_body(a[0], &a, &mut y_body);
unsafe { super::fma_axpy_into_fma_avx2(a[0], &a, &mut y_var) };
assert_eq!(
y_var.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
y_body.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"axpy seed={seed}"
);
}
}
#[test]
fn fma_dot_beats_naive_accuracy() {
let mut fma_total = 0.0f64;
let mut naive_total = 0.0f64;
let mut strict_wins = 0;
for seed in 0..64u64 {
let len = 200 + (seed as usize % 57);
let (a, b) = ill_conditioned_pair(len, 0x9E37_79B9 ^ seed.wrapping_mul(2654435761));
let truth = exact_dot(&a, &b);
let fe = (super::fma_dot(&a, &b) - truth).abs();
let ne = (naive_dot(&a, &b) - truth).abs();
let floor = 8.0 * f64::EPSILON * truth.abs();
assert!(
fe <= ne * (1.0 + 1e-6) + floor,
"fma_dot worse than naive: seed={seed} fma_err={fe:.3e} naive_err={ne:.3e}",
);
if fe < ne {
strict_wins += 1;
}
fma_total += fe;
naive_total += ne;
}
assert!(
fma_total < naive_total,
"fma_dot aggregate error {fma_total:.3e} not below naive {naive_total:.3e}",
);
assert!(
strict_wins >= 40,
"expected fma_dot to strictly win the majority; only {strict_wins}/64",
);
}
#[test]
fn fast_atv_blocked_beats_naive_accuracy() {
let n = 200_003usize;
let p = 8usize;
let mut s = 0xD1B5_4A32u64;
let mut next = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 11) as f64 / ((1u64 << 53) as f64) - 0.5
};
let mut x = Array2::<f64>::zeros((n, p));
let mut v = Array1::<f64>::zeros(n);
for i in 0..n {
let scale = 10f64.powi((i % 17) as i32 - 8);
v[i] = if i % 2 == 0 { scale } else { -scale } * next();
for j in 0..p {
x[[i, j]] = next() * scale;
}
}
let got = fast_atv(&x, &v);
let vv: Vec<f64> = v.to_vec();
let mut table: Vec<(usize, f64, f64, f64)> = Vec::with_capacity(p);
for j in 0..p {
let col: Vec<f64> = (0..n).map(|i| x[[i, j]]).collect();
let truth = dd_dot(&col, &vv);
let naive = naive_dot(&col, &vv);
table.push((j, truth, (got[j] - truth).abs(), (naive - truth).abs()));
}
let report: String = table
.iter()
.map(|&(j, truth, ge, ne)| {
format!(" col {j}: truth={truth:.6e} blocked_err={ge:.3e} naive_err={ne:.3e}\n")
})
.collect();
let mut blocked_total = 0.0f64;
let mut naive_total = 0.0f64;
for &(j, truth, ge, ne) in &table {
assert!(
ne > 0.0,
"col {j}: naive baseline error is exactly 0.0, so this column \
cannot discriminate the two reductions - the fixture is no \
longer ill-conditioned\n{report}",
);
assert!(
ge <= 64.0 * f64::EPSILON * truth.abs(),
"col {j}: blocked err {ge:.3e} exceeds naive {ne:.3e}\n{report}",
);
blocked_total += ge;
naive_total += ne;
}
assert!(
2.0 * blocked_total < naive_total,
"blocked aggregate error {blocked_total:.3e} is not at least 2x \
below naive {naive_total:.3e}; a 391-block pairwise reduction \
should be roughly sqrt(391) = 20x better\n{report}",
);
}
#[test]
fn fast_av_strided_input_matches_ndarray() {
let mut base = Array2::<f64>::zeros((40, 60));
let mut s = 0x0BAD_F00Du64;
let mut next = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 11) as f64 / ((1u64 << 53) as f64) - 0.5
};
for x in base.iter_mut() {
*x = next();
}
let a = base.t();
let mut v = Array1::<f64>::zeros(40);
for x in v.iter_mut() {
*x = next();
}
let got = fast_av(&a, &v);
let want = a.dot(&v);
assert!(
max_abs_diff_1d(&got, &want) < 1e-11,
"strided fast_av mismatch (fallback path)",
);
}
#[test]
fn faer_sequential_scope_sets_seq_inside_and_restores_after() {
crate::test_support::with_global_parallelism_serialized(|| {
let baseline = faer::get_global_parallelism();
faer::set_global_parallelism(Par::rayon(4));
assert_eq!(
faer::get_global_parallelism(),
Par::rayon(4),
"baseline must be the parallel policy we just set",
);
{
let faer_seq_guard = FaerSequentialScope::enter();
assert_eq!(
faer::get_global_parallelism(),
Par::Seq,
"faer must be pinned to Par::Seq inside the scope",
);
{
let faer_seq_inner_guard = FaerSequentialScope::enter();
assert_eq!(
faer::get_global_parallelism(),
Par::Seq,
"nested scope stays Par::Seq",
);
drop(faer_seq_inner_guard);
}
assert_eq!(
faer::get_global_parallelism(),
Par::Seq,
"inner drop must not restore while outer scope is still live",
);
drop(faer_seq_guard);
}
assert_eq!(
faer::get_global_parallelism(),
Par::rayon(4),
"outermost drop must restore the pre-scope parallelism policy",
);
let observed = with_faer_sequential(|| faer::get_global_parallelism());
assert_eq!(
observed,
Par::Seq,
"with_faer_sequential runs body under Seq"
);
assert_eq!(
faer::get_global_parallelism(),
Par::rayon(4),
"with_faer_sequential restores after the body returns",
);
faer::set_global_parallelism(baseline);
});
}
}
#[cfg(test)]
mod parallelism_snapshot_2738_tests {
use super::*;
#[test]
fn captured_snapshot_is_self_consistent() {
let snapshot =
crate::test_support::with_global_parallelism_serialized(ParallelismSnapshot::capture);
assert!(
snapshot.inconsistency().is_none(),
"the live thread configuration disagrees with itself: {} ({snapshot})",
snapshot.inconsistency().unwrap_or_default(),
);
}
#[test]
fn inconsistent_configurations_are_reported() {
let contradictory = ParallelismSnapshot::from_parts(Par::rayon(4), 4, 1, Some(4));
assert!(
contradictory.inconsistency().is_some(),
"a live FaerSequentialScope with non-sequential faer must be flagged: \
{contradictory}",
);
assert!(
ParallelismSnapshot::from_parts(Par::Seq, 0, 0, Some(1))
.inconsistency()
.is_some(),
"a zero-wide rayon pool must be flagged",
);
assert!(
ParallelismSnapshot::from_parts(Par::Seq, 1, 0, Some(0))
.inconsistency()
.is_some(),
"zero cores available to the process must be flagged",
);
assert!(
ParallelismSnapshot::from_parts(Par::rayon(4), 4, 0, Some(8))
.inconsistency()
.is_none(),
"a wide pool with no sequential scope is consistent",
);
assert!(
ParallelismSnapshot::from_parts(Par::Seq, 4, 2, None)
.inconsistency()
.is_none(),
"a pinned scope on a wide pool is consistent, and an unavailable core \
count is not itself an inconsistency",
);
}
#[test]
fn a_sequential_pin_changes_the_snapshot() {
let pinned = crate::test_support::with_global_parallelism_serialized(|| {
with_faer_sequential(ParallelismSnapshot::capture)
});
assert!(
pinned.faer_global_sequential,
"inside a FaerSequentialScope the snapshot must report faer sequential: \
{pinned}",
);
assert_eq!(
pinned.faer_global_degree, 1,
"a sequential pin is one thread of numerics: {pinned}",
);
assert!(
pinned.faer_sequential_scope_depth >= 1,
"the scope that did the pinning must be visible in the depth: {pinned}",
);
assert!(
pinned.inconsistency().is_none(),
"a pinned snapshot must still be self-consistent: {pinned}",
);
assert_eq!(
pinned.rayon_current_num_threads,
rayon::current_num_threads(),
"the pin must not be mistaken for a narrower rayon pool",
);
}
#[test]
fn rendering_carries_every_field() {
let snapshot = ParallelismSnapshot::from_parts(Par::rayon(3), 5, 2, Some(7));
let rendered = snapshot.to_string();
for field in [
"rayon_current_num_threads=5",
"faer_global_sequential=false",
"faer_global_degree=3",
"faer_sequential_scope_depth=2",
"process_available_parallelism=7",
] {
assert!(
rendered.contains(field),
"the rendered snapshot dropped `{field}`: {rendered}",
);
}
let unavailable = ParallelismSnapshot::from_parts(Par::Seq, 1, 0, None);
assert!(
unavailable.to_string().contains("unavailable"),
"a missing core count must say so rather than render as a number: \
{unavailable}",
);
}
}
#[cfg(test)]
mod eigh_ordering_contract_tests {
use super::*;
use ndarray::Array2;
#[test]
fn eigh_returns_eigenvalues_in_ascending_order() {
fn hashed_unit(seed: u64) -> f64 {
let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
((z >> 11) as f64 / (1u64 << 53) as f64) * 2.0 - 1.0
}
for &n in &[1_usize, 2, 3, 5, 6, 9, 17, 36] {
for seed in 0..8_u64 {
for &scale in &[1.0_f64, 1.0e-9, 1.0e9] {
let mut m = Array2::<f64>::zeros((n, n));
let mut k = seed.wrapping_mul(1_000_003).wrapping_add(n as u64);
for i in 0..n {
for j in 0..=i {
k = k.wrapping_add(0x1234_5678);
let value = hashed_unit(k) * scale;
m[[i, j]] = value;
m[[j, i]] = value;
}
}
let (values, _) = m.eigh(Side::Lower).expect("eigendecomposition");
assert_eq!(values.len(), n, "n={n}: one eigenvalue per dimension");
for w in 1..n {
assert!(
values[w - 1] <= values[w],
"n={n} seed={seed} scale={scale:e}: eigenvalues are NOT ascending at \
index {w} ({:e} then {:e}). `cluster_stable_eigh` scans for RUNS of \
equal eigenvalues and would silently stop finding degenerate clusters.",
values[w - 1],
values[w]
);
}
}
}
}
}
#[test]
fn equal_eigenvalues_are_returned_adjacent() {
let d = ndarray::arr1(&[2.0_f64, 7.0, 2.0, 7.0, 2.0]);
let n = d.len();
let v = ndarray::arr1(&[1.0_f64, -2.0, 3.0, -4.0, 5.0]);
let vtv: f64 = v.iter().map(|x| x * x).sum();
let mut q = Array2::<f64>::zeros((n, n));
for i in 0..n {
q[[i, i]] = 1.0;
}
for i in 0..n {
for j in 0..n {
q[[i, j]] -= 2.0 * v[i] * v[j] / vtv;
}
}
let mut a = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut acc = 0.0;
for k in 0..n {
acc += q[[i, k]] * d[k] * q[[j, k]];
}
a[[i, j]] = acc;
}
}
let (values, _) = a.eigh(Side::Lower).expect("eigendecomposition");
let low = values.iter().filter(|v| (**v - 2.0).abs() < 1.0e-9).count();
let high = values.iter().filter(|v| (**v - 7.0).abs() < 1.0e-9).count();
assert_eq!(low, 3, "planted multiplicity 3 at lambda=2, got {values:?}");
assert_eq!(high, 2, "planted multiplicity 2 at lambda=7, got {values:?}");
for w in 0..3 {
assert!(
(values[w] - 2.0).abs() < 1.0e-9,
"the three lambda=2 eigenvalues must occupy indices 0..3 contiguously, \
or `cluster_stable_eigh`'s run scan splits the cluster: {values:?}"
);
}
for w in 3..5 {
assert!(
(values[w] - 7.0).abs() < 1.0e-9,
"the two lambda=7 eigenvalues must occupy indices 3..5 contiguously: {values:?}"
);
}
}
}