use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrowSolverMode {
Direct,
SqrtBA,
InexactPCG,
}
impl ArrowSolverMode {
pub const fn automatic(k: usize) -> Self {
if k <= DIRECT_SOLVE_MAX_K {
Self::Direct
} else {
Self::InexactPCG
}
}
pub const fn automatic_for_single_precision(k: usize) -> Self {
if k <= DIRECT_SOLVE_MAX_K {
Self::SqrtBA
} else {
Self::InexactPCG
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum PcgStopReason {
#[default]
Converged,
MaxIter,
TrustRegion,
Indefinite,
Stagnation,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ArrowPcgDiagnostics {
pub iterations: usize,
pub matvec_calls: usize,
pub precond_apply_calls: usize,
pub ridge_escalations: usize,
pub final_relative_residual: f64,
pub stopping_reason: PcgStopReason,
pub mixed_precision_status: MixedPrecisionStatus,
pub used_device_arrow: bool,
pub injected_host_procedural_matvec: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MixedPrecisionStatus {
#[default]
Off,
Certified { refinement_steps: usize },
F64Fallback,
}
#[derive(Debug, Clone)]
pub struct ArrowPcgOptions {
pub max_iterations: usize,
pub relative_tolerance: f64,
}
impl Default for ArrowPcgOptions {
fn default() -> Self {
Self {
max_iterations: DEFAULT_PCG_MAX_ITERATIONS,
relative_tolerance: DEFAULT_PCG_RELATIVE_TOLERANCE,
}
}
}
#[derive(Debug, Clone)]
pub struct ArrowTrustRegionOptions {
pub radius: f64,
pub steihaug_relative_tolerance: f64,
pub max_iterations: usize,
}
impl Default for ArrowTrustRegionOptions {
fn default() -> Self {
Self {
radius: DEFAULT_TRUST_REGION_RADIUS,
steihaug_relative_tolerance: DEFAULT_PCG_RELATIVE_TOLERANCE,
max_iterations: DEFAULT_PCG_MAX_ITERATIONS,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArrowSolvePrecisionPolicy {
F64Only,
CertifiedMixed {
max_refinement_steps: usize,
residual_relative_tolerance: f64,
kappa_unit_roundoff_margin: f64,
},
}
impl Default for ArrowSolvePrecisionPolicy {
fn default() -> Self {
Self::F64Only
}
}
impl ArrowSolvePrecisionPolicy {
pub fn certified_mixed() -> Self {
Self::CertifiedMixed {
max_refinement_steps: DEFAULT_MIXED_PRECISION_MAX_REFINEMENTS,
residual_relative_tolerance: DEFAULT_MIXED_PRECISION_CERTIFICATE_TOLERANCE,
kappa_unit_roundoff_margin: DEFAULT_MIXED_PRECISION_KAPPA_MARGIN,
}
}
pub(crate) fn is_enabled(self) -> bool {
matches!(self, ArrowSolvePrecisionPolicy::CertifiedMixed { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArrowEvidencePolicy {
Strict,
PositiveDefinite,
UnitDeflation { relative_floor: f64 },
}
impl ArrowEvidencePolicy {
pub(crate) fn factors_undamped_evidence(self) -> bool {
!matches!(self, Self::Strict)
}
pub(crate) fn reduced_schur_policy(self) -> ReducedSchurPolicy {
match self {
Self::Strict | Self::PositiveDefinite => ReducedSchurPolicy::StrictNewton,
Self::UnitDeflation { relative_floor } => {
ReducedSchurPolicy::EvidenceUnitDeflation { relative_floor }
}
}
}
}
#[derive(Clone)]
pub struct ArrowSolveOptions {
pub mode: ArrowSolverMode,
pub gpu_policy: gam_gpu::GpuPolicy,
pub pcg: ArrowPcgOptions,
pub trust_region: ArrowTrustRegionOptions,
pub streaming_chunk_size: Option<usize>,
pub riemannian_trust_region: bool,
pub gpu_matvec: Option<GpuSchurMatvec>,
pub evidence_policy: ArrowEvidencePolicy,
pub solve_precision: ArrowSolvePrecisionPolicy,
pub newton_schur_tikhonov_rel_floor: Option<f64>,
pub sae_resident_frame:
Option<std::sync::Arc<dyn crate::gpu_kernels::arrow_schur::SaeResidentFrame + Send + Sync>>,
}
impl std::fmt::Debug for ArrowSolveOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArrowSolveOptions")
.field("mode", &self.mode)
.field("gpu_policy", &self.gpu_policy)
.field("pcg", &self.pcg)
.field("trust_region", &self.trust_region)
.field("streaming_chunk_size", &self.streaming_chunk_size)
.field("riemannian_trust_region", &self.riemannian_trust_region)
.field("gpu_matvec", &self.gpu_matvec.is_some())
.field("evidence_policy", &self.evidence_policy)
.field("solve_precision", &self.solve_precision)
.field(
"newton_schur_tikhonov_rel_floor",
&self.newton_schur_tikhonov_rel_floor,
)
.field("sae_resident_frame", &self.sae_resident_frame.is_some())
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ArrowProximalCorrectionOptions {
pub initial_ridge: f64,
pub ridge_growth: f64,
pub max_attempts: usize,
pub armijo_c1: f64,
pub gradient_tolerance: f64,
pub convergence_objective_rel_tol: f64,
}
impl Default for ArrowProximalCorrectionOptions {
fn default() -> Self {
Self {
initial_ridge: DEFAULT_PROXIMAL_INITIAL_RIDGE,
ridge_growth: DEFAULT_PROXIMAL_RIDGE_GROWTH,
max_attempts: DEFAULT_PROXIMAL_MAX_ATTEMPTS,
armijo_c1: DEFAULT_ARMIJO_C1,
gradient_tolerance: DEFAULT_GRADIENT_TOLERANCE,
convergence_objective_rel_tol: DEFAULT_PROXIMAL_CONVERGENCE_REL_TOL,
}
}
}
#[derive(Debug, Clone)]
pub struct ArrowAcceptedProximalStep {
pub delta_t: Array1<f64>,
pub delta_beta: Array1<f64>,
pub ridge_t: f64,
pub ridge_beta: f64,
pub proximal_ridge: f64,
pub objective_value: f64,
pub trial_objective_value: f64,
pub gradient_dot_step: f64,
pub attempts: usize,
}
impl ArrowSolveOptions {
pub fn automatic(k: usize) -> Self {
Self {
mode: ArrowSolverMode::automatic(k),
gpu_policy: gam_gpu::GpuPolicy::Auto,
pcg: ArrowPcgOptions::default(),
trust_region: ArrowTrustRegionOptions::default(),
streaming_chunk_size: None,
riemannian_trust_region: false,
gpu_matvec: None,
evidence_policy: ArrowEvidencePolicy::Strict,
solve_precision: ArrowSolvePrecisionPolicy::F64Only,
newton_schur_tikhonov_rel_floor: None,
sae_resident_frame: None,
}
}
pub fn direct() -> Self {
Self {
mode: ArrowSolverMode::Direct,
gpu_policy: gam_gpu::GpuPolicy::Auto,
pcg: ArrowPcgOptions::default(),
trust_region: ArrowTrustRegionOptions::default(),
streaming_chunk_size: None,
riemannian_trust_region: false,
gpu_matvec: None,
evidence_policy: ArrowEvidencePolicy::Strict,
solve_precision: ArrowSolvePrecisionPolicy::F64Only,
newton_schur_tikhonov_rel_floor: None,
sae_resident_frame: None,
}
}
pub fn sqrt_ba() -> Self {
Self {
mode: ArrowSolverMode::SqrtBA,
gpu_policy: gam_gpu::GpuPolicy::Auto,
pcg: ArrowPcgOptions::default(),
trust_region: ArrowTrustRegionOptions::default(),
streaming_chunk_size: None,
riemannian_trust_region: false,
gpu_matvec: None,
evidence_policy: ArrowEvidencePolicy::Strict,
solve_precision: ArrowSolvePrecisionPolicy::F64Only,
newton_schur_tikhonov_rel_floor: None,
sae_resident_frame: None,
}
}
pub fn inexact_pcg() -> Self {
Self {
mode: ArrowSolverMode::InexactPCG,
gpu_policy: gam_gpu::GpuPolicy::Auto,
pcg: ArrowPcgOptions::default(),
trust_region: ArrowTrustRegionOptions::default(),
streaming_chunk_size: None,
riemannian_trust_region: false,
gpu_matvec: None,
evidence_policy: ArrowEvidencePolicy::Strict,
solve_precision: ArrowSolvePrecisionPolicy::F64Only,
newton_schur_tikhonov_rel_floor: None,
sae_resident_frame: None,
}
}
pub fn with_streaming_chunk_size(mut self, chunk_size: Option<usize>) -> Self {
self.streaming_chunk_size = chunk_size.filter(|&chunk| chunk > 0);
self
}
pub fn with_gpu_policy(mut self, gpu_policy: gam_gpu::GpuPolicy) -> Self {
self.gpu_policy = gpu_policy;
self
}
pub fn with_positive_definite_evidence(mut self) -> Self {
self.evidence_policy = ArrowEvidencePolicy::PositiveDefinite;
self
}
pub fn with_evidence_unit_deflation(mut self, relative_floor: f64) -> Self {
self.evidence_policy = ArrowEvidencePolicy::UnitDeflation { relative_floor };
self
}
pub fn with_newton_schur_tikhonov(mut self, relative_floor: f64) -> Self {
self.newton_schur_tikhonov_rel_floor = Some(relative_floor);
self
}
pub fn with_solve_precision_policy(mut self, policy: ArrowSolvePrecisionPolicy) -> Self {
self.solve_precision = policy;
self
}
#[must_use]
pub fn with_streaming_solve_precision_default(&self) -> Self {
let mut out = self.clone();
if matches!(out.solve_precision, ArrowSolvePrecisionPolicy::F64Only) {
out.solve_precision = ArrowSolvePrecisionPolicy::certified_mixed();
}
out
}
}
pub trait BatchedBlockSolver {
fn factor_blocks(
&self,
rows: &[ArrowRowBlock],
ridge_t: f64,
d: usize,
evidence_factorization: bool,
) -> Result<ArrowFactorSlab, ArrowSchurError>;
fn factor_blocks_with_policy(
&self,
rows: &[ArrowRowBlock],
ridge_t: f64,
d: usize,
evidence_factorization: bool,
gpu_policy: gam_gpu::GpuPolicy,
) -> Result<ArrowFactorSlab, ArrowSchurError> {
match gpu_policy {
gam_gpu::GpuPolicy::Auto
| gam_gpu::GpuPolicy::Off
| gam_gpu::GpuPolicy::Required => {
self.factor_blocks(rows, ridge_t, d, evidence_factorization)
}
}
}
fn solve_block_vector(
&self,
factor: ArrayView2<'_, f64>,
rhs: ArrayView1<'_, f64>,
) -> Array1<f64>;
fn solve_block_matrix(
&self,
factor: ArrayView2<'_, f64>,
rhs: ArrayView2<'_, f64>,
) -> Array2<f64>;
fn sqrt_solve_block_matrix(
&self,
factor: ArrayView2<'_, f64>,
rhs: ArrayView2<'_, f64>,
) -> Array2<f64>;
fn block_gemm_subtract(&self, schur: &mut Array2<f64>, left: &Array2<f64>, right: &Array2<f64>);
}
#[derive(Debug, Clone)]
pub struct ArrowRowGaugeDeflation {
pub directions: Arc<[Vec<Array1<f64>>]>,
}
#[derive(Debug, Clone)]
pub struct ArrowBetaGaugeQuotient {
pub directions: Arc<[Array1<f64>]>,
}
impl ArrowBetaGaugeQuotient {
pub fn new(directions: Vec<Array1<f64>>) -> Result<Self, String> {
if directions.is_empty() {
return Err("ArrowBetaGaugeQuotient requires at least one direction".to_string());
}
let dim = directions[0].len();
if dim == 0 {
return Err("ArrowBetaGaugeQuotient directions must be non-empty".to_string());
}
let mut basis: Vec<Array1<f64>> = Vec::with_capacity(directions.len());
for (direction_idx, mut direction) in directions.into_iter().enumerate() {
if direction.len() != dim {
return Err(format!(
"ArrowBetaGaugeQuotient direction {direction_idx} length {} != {dim}",
direction.len()
));
}
if direction.iter().any(|value| !value.is_finite()) {
return Err(format!(
"ArrowBetaGaugeQuotient direction {direction_idx} contains a non-finite value"
));
}
for existing in &basis {
let coefficient = direction.dot(existing);
direction.scaled_add(-coefficient, existing);
}
let norm_sq = direction.dot(&direction);
if !(norm_sq.is_finite() && norm_sq > 0.0) {
return Err(format!(
"ArrowBetaGaugeQuotient direction {direction_idx} is zero or linearly dependent"
));
}
direction *= norm_sq.sqrt().recip();
basis.push(direction);
}
Ok(Self {
directions: Arc::from(basis.into_boxed_slice()),
})
}
pub fn dimension(&self) -> usize {
self.directions.len()
}
pub(crate) fn border_dim(&self) -> usize {
self.directions[0].len()
}
pub fn project_complement(&self, x: ArrayView1<'_, f64>) -> Array1<f64> {
assert_eq!(x.len(), self.border_dim());
let mut out = x.to_owned();
for direction in self.directions.iter() {
let coefficient = out.dot(direction);
out.scaled_add(-coefficient, direction);
}
out
}
pub fn pin_reduced_schur(&self, schur: ArrayView2<'_, f64>) -> Array2<f64> {
let dim = self.border_dim();
assert_eq!(schur.dim(), (dim, dim));
let mut right = schur.to_owned();
for direction in self.directions.iter() {
let schur_q = right.dot(direction);
for row in 0..dim {
for col in 0..dim {
right[[row, col]] -= schur_q[row] * direction[col];
}
}
}
let mut pinned = right;
for direction in self.directions.iter() {
let q_t_s = direction.dot(&pinned);
for row in 0..dim {
for col in 0..dim {
pinned[[row, col]] -= direction[row] * q_t_s[col];
}
}
}
for direction in self.directions.iter() {
for row in 0..dim {
for col in 0..dim {
pinned[[row, col]] += direction[row] * direction[col];
}
}
}
for row in 0..dim {
for col in (row + 1)..dim {
let value = 0.5 * (pinned[[row, col]] + pinned[[col, row]]);
pinned[[row, col]] = value;
pinned[[col, row]] = value;
}
}
pinned
}
}
impl ArrowRowGaugeDeflation {
pub fn new(directions: Vec<Vec<Array1<f64>>>) -> Self {
Self {
directions: Arc::from(directions.into_boxed_slice()),
}
}
pub(crate) fn row(&self, row: usize) -> &[Array1<f64>] {
self.directions.get(row).map(Vec::as_slice).unwrap_or(&[])
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CpuBatchedBlockSolver;
impl BatchedBlockSolver for CpuBatchedBlockSolver {
fn factor_blocks(
&self,
rows: &[ArrowRowBlock],
ridge_t: f64,
d: usize,
evidence_factorization: bool,
) -> Result<ArrowFactorSlab, ArrowSchurError> {
self.factor_blocks_with_policy(
rows,
ridge_t,
d,
evidence_factorization,
gam_gpu::global_policy(),
)
}
fn factor_blocks_with_policy(
&self,
rows: &[ArrowRowBlock],
ridge_t: f64,
d: usize,
evidence_factorization: bool,
gpu_policy: gam_gpu::GpuPolicy,
) -> Result<ArrowFactorSlab, ArrowSchurError> {
if let Some(batched) =
try_factor_blocks_batched(rows, ridge_t, d, evidence_factorization, gpu_policy)?
{
return Ok(batched);
}
let n = rows.len();
let parallel =
n >= SCHUR_MATVEC_PARALLEL_ROW_MIN && rayon::current_thread_index().is_none();
let out = if parallel {
use rayon::prelude::*;
(0..n)
.into_par_iter()
.map(|row_idx| {
gam_problem::with_nested_parallel(|| {
factor_one_row(&rows[row_idx], ridge_t, d, row_idx, evidence_factorization)
})
})
.collect::<Result<Vec<_>, ArrowSchurError>>()?
} else {
let mut out = Vec::with_capacity(n);
for (row_idx, row) in rows.iter().enumerate() {
out.push(factor_one_row(
row,
ridge_t,
d,
row_idx,
evidence_factorization,
)?);
}
out
};
Ok(ArrowFactorSlab::from_blocks(out))
}
fn solve_block_vector(
&self,
factor: ArrayView2<'_, f64>,
rhs: ArrayView1<'_, f64>,
) -> Array1<f64> {
match (factor.nrows(), factor.ncols(), rhs.len()) {
(1, 1, 1) => cholesky_solve_vector_fixed::<1>(factor, rhs),
(2, 2, 2) => cholesky_solve_vector_fixed::<2>(factor, rhs),
(3, 3, 3) => cholesky_solve_vector_fixed::<3>(factor, rhs),
(4, 4, 4) => cholesky_solve_vector_fixed::<4>(factor, rhs),
_ => cholesky_solve_vector(factor, rhs),
}
}
fn solve_block_matrix(
&self,
factor: ArrayView2<'_, f64>,
rhs: ArrayView2<'_, f64>,
) -> Array2<f64> {
cholesky_solve_matrix(factor, rhs)
}
fn sqrt_solve_block_matrix(
&self,
factor: ArrayView2<'_, f64>,
rhs: ArrayView2<'_, f64>,
) -> Array2<f64> {
forward_substitution_lower_matrix(factor, rhs)
}
fn block_gemm_subtract(
&self,
schur: &mut Array2<f64>,
left: &Array2<f64>,
right: &Array2<f64>,
) {
let k = schur.nrows();
let d = left.nrows();
assert_eq!(left.ncols(), k);
assert_eq!(right.nrows(), d);
assert_eq!(right.ncols(), k);
assert_eq!(schur.ncols(), k);
let mut left_active = Vec::with_capacity(k);
let mut right_active = Vec::with_capacity(k);
let schur_cols = schur.ncols();
let schur_flat = schur
.as_slice_mut()
.expect("block_gemm_subtract: reduced Schur must be standard-layout");
for c in 0..d {
left_active.clear();
right_active.clear();
let left_row = left.row(c);
let right_row = right.row(c);
let left_row = left_row
.as_slice()
.expect("block_gemm_subtract: left row must be contiguous");
let right_row = right_row
.as_slice()
.expect("block_gemm_subtract: right row must be contiguous");
for col in 0..k {
let l = left_row[col];
let r = right_row[col];
if l != 0.0 {
left_active.push((col, l));
}
if r != 0.0 {
right_active.push((col, r));
}
}
if left_active.is_empty() || right_active.is_empty() {
continue;
}
for &(a, lca) in &left_active {
let row_off = a * schur_cols;
if right_active.len() == k {
let schur_row = &mut schur_flat[row_off..row_off + k];
for (s, &r) in schur_row.iter_mut().zip(right_row) {
*s -= lca * r;
}
} else {
for &(b, rcb) in &right_active {
schur_flat[row_off + b] -= lca * rcb;
}
}
}
}
}
}