Skip to main content

gam_solve/gpu/
reml_gpu.rs

1//! Exact GPU REML evidence + derivative gradient.
2//!
3//! Refactor (Block 2.1, math team section 18): the penalized Hessian `H` is
4//! Cholesky-factored exactly **once** on device, the factor is held resident,
5//! and every derivative Hessian `H_j` is solved through the cached factor
6//! with a single batched `potrs` call (`nrhs = d_rho · p`). Previously each
7//! derivative re-issued the full `cholesky_solve_gpu` path, which uploaded
8//! `H`, allocated and ran `potrf`, and downloaded the factor again — turning
9//! a `p^3 + d·p^3` workload into a `(d+1)·p^3` one and serializing `d_rho`
10//! factor passes onto the device.
11//!
12//! On the non-Linux fallback the same `cholesky_solve_gpu` path is exercised
13//! via `pirls_gpu::cholesky_solve_gpu`, so behaviour outside Linux is
14//! numerically identical (with the same per-derivative overhead) — the
15//! optimisation is Linux-only because that is where CUDA actually runs.
16
17use ndarray::{Array1, ArrayView2};
18
19#[derive(Clone, Debug)]
20pub struct RemlGpuInput<'a> {
21    pub penalized_hessian: ArrayView2<'a, f64>,
22    pub derivative_hessians: Vec<ArrayView2<'a, f64>>,
23}
24
25#[derive(Clone, Debug)]
26pub struct RemlGpuEvidence {
27    pub logdet_hessian: f64,
28    pub gradient_rho: Array1<f64>,
29}
30
31pub fn evidence_derivatives_gpu(input: RemlGpuInput<'_>) -> Result<RemlGpuEvidence, String> {
32    let p = input.penalized_hessian.nrows();
33    if p != input.penalized_hessian.ncols() {
34        return Err("REML GPU Hessian must be square".to_string());
35    }
36    for (j, derivative) in input.derivative_hessians.iter().enumerate() {
37        if derivative.dim() != (p, p) {
38            return Err(format!(
39                "REML derivative Hessian {j} has shape {:?}, expected {p}x{p}",
40                derivative.dim()
41            ));
42        }
43    }
44
45    #[cfg(target_os = "linux")]
46    {
47        if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
48            .map_err(|error| error.to_string())?
49            .is_some()
50        {
51            return linux_cuda::evidence_derivatives(input);
52        }
53    }
54
55    cpu_fallback::evidence_derivatives(input)
56}
57
58#[cfg(target_os = "linux")]
59mod linux_cuda {
60    use super::{RemlGpuEvidence, RemlGpuInput};
61    use cudarc::cusolver::DnHandle;
62    use gam_gpu::driver::to_col_major;
63    use gam_gpu::solver::{
64        cholesky_logdet_from_col_major, context_and_stream, pinned_htod, potrf_in_place,
65        potrs_in_place,
66    };
67    use ndarray::Array1;
68
69    pub(super) fn evidence_derivatives(input: RemlGpuInput<'_>) -> Result<RemlGpuEvidence, String> {
70        let p = input.penalized_hessian.nrows();
71        let d = input.derivative_hessians.len();
72        let (_, stream) = context_and_stream()?;
73        let solver = DnHandle::new(stream.clone()).map_err(|e| format!("cusolver init: {e}"))?;
74
75        // Upload H once and factor in-place.
76        let h_col = to_col_major(&input.penalized_hessian);
77        let mut h_dev = pinned_htod(&stream, &h_col)?;
78        potrf_in_place(&solver, &stream, p, &mut h_dev)?;
79        let factor_col = stream
80            .clone_dtoh(&h_dev)
81            .map_err(|e| format!("download Cholesky factor: {e}"))?;
82        let logdet_hessian = cholesky_logdet_from_col_major(&factor_col, p);
83
84        if d == 0 {
85            return Ok(RemlGpuEvidence {
86                logdet_hessian,
87                gradient_rho: Array1::<f64>::zeros(0),
88            });
89        }
90
91        // Stack all derivative Hessians column-wise into ONE rhs of width d*p
92        // and solve with a single batched potrs against the cached factor.
93        let total_cols = p
94            .checked_mul(d)
95            .ok_or_else(|| format!("REML GPU RHS width overflow: p={p}, d={d}"))?;
96        let total_elems = p
97            .checked_mul(total_cols)
98            .ok_or_else(|| format!("REML GPU RHS size overflow: p={p}, cols={total_cols}"))?;
99        let mut rhs_col = Vec::<f64>::with_capacity(total_elems);
100        for derivative in &input.derivative_hessians {
101            let col = to_col_major(derivative);
102            rhs_col.extend_from_slice(&col);
103        }
104        let mut rhs_dev = pinned_htod(&stream, &rhs_col)?;
105        potrs_in_place(&solver, &stream, p, total_cols, &h_dev, &mut rhs_dev)?;
106        let solved_col = stream
107            .clone_dtoh(&rhs_dev)
108            .map_err(|e| format!("download REML derivative solves: {e}"))?;
109
110        let mut gradient_rho = Array1::<f64>::zeros(d);
111        for j in 0..d {
112            let offset = j * p * p;
113            // Diagonal of H^{-1} A_j is the diagonal of the j-th p*p slab.
114            let mut trace = 0.0_f64;
115            for i in 0..p {
116                trace += solved_col[offset + i * p + i];
117            }
118            gradient_rho[j] = 0.5 * trace;
119        }
120
121        Ok(RemlGpuEvidence {
122            logdet_hessian,
123            gradient_rho,
124        })
125    }
126}
127
128mod cpu_fallback {
129    use super::{RemlGpuEvidence, RemlGpuInput};
130    use ndarray::{Array1, Array2};
131
132    pub(super) fn evidence_derivatives(input: RemlGpuInput<'_>) -> Result<RemlGpuEvidence, String> {
133        let p = input.penalized_hessian.nrows();
134        let mut identity = Array2::<f64>::zeros((p, p));
135        for i in 0..p {
136            identity[[i, i]] = 1.0;
137        }
138        let (_, logdet_hessian) =
139            crate::gpu::pirls_gpu::cholesky_solve_gpu(input.penalized_hessian, identity.view())?;
140        let mut gradient_rho = Array1::<f64>::zeros(input.derivative_hessians.len());
141        for (j, derivative) in input.derivative_hessians.iter().enumerate() {
142            let (solved, _) = crate::gpu::pirls_gpu::cholesky_solve_gpu(
143                input.penalized_hessian,
144                derivative.view(),
145            )?;
146            let mut trace = 0.0_f64;
147            for i in 0..p {
148                trace += solved[[i, i]];
149            }
150            gradient_rho[j] = 0.5 * trace;
151        }
152        Ok(RemlGpuEvidence {
153            logdet_hessian,
154            gradient_rho,
155        })
156    }
157}