1use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct PirlsLoopLikelihoodScale(PirlsLoopLikelihoodScaleKind);
12
13#[derive(Clone, Copy, Debug, PartialEq)]
14enum PirlsLoopLikelihoodScaleKind {
15 NonGamma,
16 GammaShape(f64),
17}
18
19impl PirlsLoopLikelihoodScale {
20 #[inline]
21 pub const fn non_gamma() -> Self {
22 Self(PirlsLoopLikelihoodScaleKind::NonGamma)
23 }
24
25 pub fn gamma_shape(shape: f64) -> Result<Self, String> {
26 if shape.is_finite() && shape > 0.0 {
27 Ok(Self(PirlsLoopLikelihoodScaleKind::GammaShape(shape)))
28 } else {
29 Err(format!(
30 "GPU PIRLS Gamma shape must be finite and strictly positive, got {shape:?}"
31 ))
32 }
33 }
34
35 #[cfg(target_os = "linux")]
36 fn kernel_argument(
37 self,
38 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
39 ) -> Result<f64, String> {
40 use crate::gpu_kernels::pirls_row::PirlsRowFamily;
41 match (family, self.0) {
42 (PirlsRowFamily::GammaLog, PirlsLoopLikelihoodScaleKind::GammaShape(shape)) => {
43 Ok(shape)
44 }
45 (PirlsRowFamily::GammaLog, PirlsLoopLikelihoodScaleKind::NonGamma) => {
46 Err("GPU Gamma row kernel requires an explicit resolved Gamma shape".to_string())
47 }
48 (_, PirlsLoopLikelihoodScaleKind::NonGamma) => Ok(f64::NAN),
49 (_, PirlsLoopLikelihoodScaleKind::GammaShape(shape)) => Err(format!(
50 "GPU non-Gamma row kernel {family:?} received Gamma shape {shape:?}"
51 )),
52 }
53 }
54}
55
56#[derive(Clone, Debug)]
57pub struct PirlsGpuInput<'a> {
58 pub x: ArrayView2<'a, f64>,
59 pub weights: ArrayView1<'a, f64>,
60 pub penalty_hessian: ArrayView2<'a, f64>,
61 pub gradient: ArrayView1<'a, f64>,
65 pub step_lm_lambda: f64,
69 pub objective_ridge: f64,
72}
73
74#[derive(Clone, Debug)]
75pub struct PirlsGpuStep {
76 pub penalized_hessian: Array2<f64>,
77 pub direction: Array1<f64>,
78 pub logdet: f64,
79}
80
81#[derive(Clone, Debug)]
89pub struct PirlsStepStreamInput<'a> {
90 pub weights: ArrayView1<'a, f64>,
91 pub penalty_hessian: ArrayView2<'a, f64>,
92 pub gradient: ArrayView1<'a, f64>,
93 pub step_lm_lambda: f64,
96 pub objective_ridge: f64,
99}
100
101#[cfg(target_os = "linux")]
111pub struct PirlsStepStreamDeviceInput<'a, 'b> {
112 pub w_solver_dev: &'a cudarc::driver::CudaSlice<f64>,
115 pub grad_eta_dev: &'b cudarc::driver::CudaSlice<f64>,
118 pub penalty_hessian: ArrayView2<'b, f64>,
120 pub step_lm_lambda: f64,
123 pub objective_ridge: f64,
126 pub beta_dev: &'b cudarc::driver::CudaSlice<f64>,
130 pub linear_shift: ArrayView1<'b, f64>,
133}
134
135#[cfg(target_os = "linux")]
143pub struct PirlsGpuSharedData {
144 pub(crate) ctx: std::sync::Arc<cudarc::driver::CudaContext>,
145 pub(crate) n: usize,
146 pub(crate) p: usize,
147 pub(crate) x_original_dev: cudarc::driver::CudaSlice<f64>,
150 pub(crate) y_dev: cudarc::driver::CudaSlice<f64>,
152 pub(crate) prior_w_dev: cudarc::driver::CudaSlice<f64>,
154 pub(crate) offset_dev: cudarc::driver::CudaSlice<f64>,
156}
157
158#[cfg(target_os = "linux")]
172pub struct SigmaPirlsGpuWorkspace {
173 pub(crate) stream: std::sync::Arc<cudarc::driver::CudaStream>,
174 pub(crate) blas: cudarc::cublas::CudaBlas,
175 pub(crate) solver: cudarc::cusolver::DnHandle,
176 pub(crate) wx_dev: Option<cudarc::driver::CudaSlice<f64>>,
179 pub(crate) w_dev: cudarc::driver::CudaSlice<f64>,
180 pub(crate) xtwx_dev: cudarc::driver::CudaSlice<f64>,
182 pub(crate) h_dev: cudarc::driver::CudaSlice<f64>,
183 pub(crate) rhs_dev: cudarc::driver::CudaSlice<f64>,
184 pub(crate) penalty_dev: cudarc::driver::CudaSlice<f64>,
185 pub(crate) qs_dev: cudarc::driver::CudaSlice<f64>,
190 pub(crate) qs_tmp_dev: cudarc::driver::CudaSlice<f64>,
193 pub(crate) beta_orig_dev: cudarc::driver::CudaSlice<f64>,
195 pub(crate) dir_orig_dev: cudarc::driver::CudaSlice<f64>,
197 pub(crate) potrf_work_dev: cudarc::driver::CudaSlice<f64>,
200 pub(crate) potrf_lwork: i32,
203 pub(crate) potrf_info_dev: cudarc::driver::CudaSlice<i32>,
207 pub(crate) potrs_info_dev: cudarc::driver::CudaSlice<i32>,
209 pub(crate) n: usize,
210 pub(crate) p: usize,
211}
212
213#[cfg(target_os = "linux")]
214pub(crate) mod cuda {
215 use super::{
216 PirlsGpuInput, PirlsGpuSharedData, PirlsGpuStep, PirlsStepStreamDeviceInput,
217 PirlsStepStreamInput, SigmaPirlsGpuWorkspace,
218 };
219 use cudarc::cublas::sys::{
220 cublasDdgmm, cublasDgeam, cublasOperation_t, cublasSideMode_t, cublasStatus_t,
221 };
222 use cudarc::cublas::{CudaBlas, Gemm, GemmConfig, Gemv, GemvConfig};
223 use cudarc::cusolver::DnHandle;
224 use cudarc::driver::{CudaSlice, DevicePtr, DevicePtrMut, LaunchConfig, PushKernelArg};
225 use gam_gpu::device_cache::PtxModuleCache;
226 use gam_gpu::driver::{from_col_major, to_col_major};
227 use gam_gpu::solver::{
228 check_deferred_potrf_info, check_deferred_potrs_info, context_and_stream, pinned_htod,
229 potrf_in_place_reuse, potrf_query_lwork, potrs_in_place_reuse,
230 };
231 use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
232
233 #[derive(Debug)]
238 pub enum PirlsGpuLoopError {
239 Geometry(gam_problem::EstimationError),
240 Runtime(String),
241 }
242
243 impl std::fmt::Display for PirlsGpuLoopError {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match self {
246 Self::Geometry(error) => write!(f, "{error}"),
247 Self::Runtime(message) => f.write_str(message),
248 }
249 }
250 }
251
252 impl std::error::Error for PirlsGpuLoopError {}
253
254 impl From<String> for PirlsGpuLoopError {
255 fn from(message: String) -> Self {
256 Self::Runtime(message)
257 }
258 }
259
260 impl From<&str> for PirlsGpuLoopError {
261 fn from(message: &str) -> Self {
262 Self::Runtime(message.to_owned())
263 }
264 }
265
266 impl From<gam_problem::EstimationError> for PirlsGpuLoopError {
267 fn from(error: gam_problem::EstimationError) -> Self {
268 Self::Geometry(error)
269 }
270 }
271
272 const CHOL_LOGDET_PTX_SOURCE: &str = r#"
279extern "C" __global__ void chol_logdet_col_major(
280 const double* __restrict__ factor,
281 int p,
282 double* __restrict__ out
283) {
284 if (threadIdx.x != 0 || blockIdx.x != 0) return;
285 double acc = 0.0;
286 long long pp = (long long)p;
287 for (long long i = 0; i < pp; ++i) {
288 acc += log(factor[i * pp + i]);
289 }
290 out[0] = 2.0 * acc;
291}
292"#;
293
294 static CHOL_LOGDET_CACHE: PtxModuleCache = PtxModuleCache::new();
295
296 const FUSED_XTWX_P_THRESHOLD: usize = 256;
301
302 const FUSED_XTWX_PTX_SOURCE: &str = concat!(
312 "extern \"C\" __global__ void xtwx_lower(",
318 "const double* __restrict__ X,",
319 "const double* __restrict__ w,",
320 "double* __restrict__ A,",
321 "int n, int p) {",
322 "int t=blockIdx.x*blockDim.x+threadIdx.x;",
323 "int np=p*(p+1)/2; if(t>=np)return;",
324 "int jv=(int)((__dsqrt_rn((double)(8*t+1))-1.0)*0.5);",
326 "while((long long)(jv+1)*(jv+2)/2<=t)jv++;",
327 "while(jv>0&&(long long)jv*(jv+1)/2>t)jv--;",
328 "int kv=t-(int)((long long)jv*(jv+1)/2);",
329 "double acc=0.0;",
330 "const double*Xj=X+(long long)jv*n;",
331 "const double*Xk=X+(long long)kv*n;",
332 "for(int i=0;i<n;++i)acc+=w[i]*Xj[i]*Xk[i];",
333 "A[jv+(long long)kv*p]=acc;}",
335 "extern \"C\" __global__ void xtscore(",
337 "const double* __restrict__ X,",
338 "const double* __restrict__ score,",
339 "double* __restrict__ s,",
340 "int n, int p) {",
341 "int j=blockIdx.x*blockDim.x+threadIdx.x;",
342 "if(j>=p)return;",
343 "double acc=0.0;",
344 "const double*Xj=X+(long long)j*n;",
345 "for(int i=0;i<n;++i)acc+=score[i]*Xj[i];",
346 "s[j]=acc;}",
347 "extern \"C\" __global__ void symmetrize_lower(",
352 "double* __restrict__ A, int p) {",
353 "int ns=p*(p-1)/2;",
354 "int t=blockIdx.x*blockDim.x+threadIdx.x;",
355 "if(t>=ns)return;",
356 "int jv=(int)((__dsqrt_rn((double)(8*t+1))+1.0)*0.5);",
358 "while((long long)jv*(jv-1)/2>t)jv--;",
359 "while((long long)(jv+1)*jv/2<=t)jv++;",
360 "int kv=t-(int)((long long)jv*(jv-1)/2);",
361 "A[kv+(long long)jv*p]=A[jv+(long long)kv*p];}",
363 );
364
365 static FUSED_XTWX_CACHE: PtxModuleCache = PtxModuleCache::new();
366
367 impl PirlsGpuSharedData {
368 pub(crate) fn upload_impl(
372 x: ArrayView2<'_, f64>,
373 y: ArrayView1<'_, f64>,
374 prior_w: ArrayView1<'_, f64>,
375 offset: ArrayView1<'_, f64>,
376 ) -> Result<Self, String> {
377 let (n, p) = x.dim();
378 if n == 0 || p == 0 {
379 return Err("empty design cannot be uploaded".to_string());
380 }
381 if y.len() != n || prior_w.len() != n || offset.len() != n {
382 return Err(format!(
383 "y/prior_w/offset length mismatch (y={}, w={}, offset={}, n={n})",
384 y.len(),
385 prior_w.len(),
386 offset.len()
387 ));
388 }
389 let (ctx, stream) = context_and_stream()?;
390 let x_col = to_col_major(&x);
391 let x_original_dev = pinned_htod(&stream, &x_col)?;
392 let y_dev = pinned_htod(&stream, y.as_slice().ok_or("y not contiguous")?)?;
393 let prior_w_dev =
394 pinned_htod(&stream, prior_w.as_slice().ok_or("prior_w not contiguous")?)?;
395 let offset_dev =
396 pinned_htod(&stream, offset.as_slice().ok_or("offset not contiguous")?)?;
397 stream
401 .synchronize()
402 .map_err(|e| format!("cuda sync after model upload: {e}"))?;
403 Ok(Self {
404 ctx,
405 n,
406 p,
407 x_original_dev,
408 y_dev,
409 prior_w_dev,
410 offset_dev,
411 })
412 }
413 }
414
415 impl SigmaPirlsGpuWorkspace {
416 pub(crate) fn allocate_impl(shared: &PirlsGpuSharedData) -> Result<Self, String> {
422 let n = shared.n;
423 let p = shared.p;
424 let stream = shared
425 .ctx
426 .new_stream()
427 .map_err(|e| format!("cuda stream alloc: {e}"))?;
428 let blas = CudaBlas::new(stream.clone()).map_err(|e| format!("cublas init: {e}"))?;
429 let solver =
430 DnHandle::new(stream.clone()).map_err(|e| format!("cusolver init: {e}"))?;
431 let np = n.checked_mul(p).ok_or("X size overflow")?;
432 let pp = p.checked_mul(p).ok_or("H size overflow")?;
433 let wx_dev = if p >= FUSED_XTWX_P_THRESHOLD {
435 Some(
436 stream
437 .alloc_zeros::<f64>(np)
438 .map_err(|e| format!("cuda alloc WX: {e}"))?,
439 )
440 } else {
441 None
442 };
443 let w_dev = stream
444 .alloc_zeros::<f64>(n)
445 .map_err(|e| format!("cuda alloc W: {e}"))?;
446 let xtwx_dev = stream
447 .alloc_zeros::<f64>(pp)
448 .map_err(|e| format!("cuda alloc XtWX: {e}"))?;
449 let h_dev = stream
450 .alloc_zeros::<f64>(pp)
451 .map_err(|e| format!("cuda alloc H: {e}"))?;
452 let rhs_dev = stream
453 .alloc_zeros::<f64>(p)
454 .map_err(|e| format!("cuda alloc RHS: {e}"))?;
455 let penalty_dev = stream
456 .alloc_zeros::<f64>(pp)
457 .map_err(|e| format!("cuda alloc penalty: {e}"))?;
458 let mut qs_dev = stream
460 .alloc_zeros::<f64>(pp)
461 .map_err(|e| format!("cuda alloc Qs: {e}"))?;
462 {
464 let mut qs_host = vec![0.0_f64; pp];
465 for i in 0..p {
466 qs_host[i * p + i] = 1.0;
467 }
468 stream
469 .memcpy_htod(&qs_host, &mut qs_dev)
470 .map_err(|e| format!("init Qs identity: {e}"))?;
471 }
472 let qs_tmp_dev = stream
473 .alloc_zeros::<f64>(pp)
474 .map_err(|e| format!("cuda alloc Qs tmp: {e}"))?;
475 let beta_orig_dev = stream
476 .alloc_zeros::<f64>(p)
477 .map_err(|e| format!("cuda alloc beta_orig: {e}"))?;
478 let dir_orig_dev = stream
479 .alloc_zeros::<f64>(p)
480 .map_err(|e| format!("cuda alloc dir_orig: {e}"))?;
481 let potrf_lwork_usize = potrf_query_lwork(&solver, &stream, p)?;
485 let potrf_lwork = i32::try_from(potrf_lwork_usize)
486 .map_err(|_| format!("potrf lwork {potrf_lwork_usize} exceeds i32"))?;
487 let alloc_len = potrf_lwork_usize.max(1);
490 let potrf_work_dev = stream
491 .alloc_zeros::<f64>(alloc_len)
492 .map_err(|e| format!("cuda alloc potrf workspace: {e}"))?;
493 let potrf_info_dev = stream
494 .alloc_zeros::<i32>(1)
495 .map_err(|e| format!("cuda alloc potrf info: {e}"))?;
496 let potrs_info_dev = stream
497 .alloc_zeros::<i32>(1)
498 .map_err(|e| format!("cuda alloc potrs info: {e}"))?;
499 Ok(Self {
500 stream,
501 blas,
502 solver,
503 wx_dev,
504 w_dev,
505 xtwx_dev,
506 h_dev,
507 rhs_dev,
508 penalty_dev,
509 qs_dev,
510 qs_tmp_dev,
511 beta_orig_dev,
512 dir_orig_dev,
513 potrf_work_dev,
514 potrf_lwork,
515 potrf_info_dev,
516 potrs_info_dev,
517 n,
518 p,
519 })
520 }
521 }
522
523 pub(super) fn upload_qs(
527 ws: &mut SigmaPirlsGpuWorkspace,
528 qs: ArrayView2<'_, f64>,
529 ) -> Result<(), String> {
530 let p = ws.p;
531 if qs.dim() != (p, p) {
532 return Err(format!("upload_qs: Qs shape {:?} != ({p},{p})", qs.dim()));
533 }
534 let qs_col = to_col_major(&qs);
535 ws.stream
536 .memcpy_htod(qs_col.as_ref(), &mut ws.qs_dev)
537 .map_err(|e| format!("upload Qs: {e}"))
538 }
539
540 pub(super) fn upload_qs_identity(ws: &mut SigmaPirlsGpuWorkspace) -> Result<(), String> {
542 let p = ws.p;
543 let pp = p * p;
544 let mut qs_host = vec![0.0_f64; pp];
545 for i in 0..p {
546 qs_host[i * p + i] = 1.0;
547 }
548 ws.stream
549 .memcpy_htod(&qs_host, &mut ws.qs_dev)
550 .map_err(|e| format!("upload Qs identity: {e}"))
551 }
552
553 fn newton_step_refine_once(
562 solver: &cudarc::cusolver::DnHandle,
563 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
564 p: usize,
565 chol_factor_dev: &CudaSlice<f64>,
566 rhs_dev: &mut CudaSlice<f64>,
567 potrs_info_dev: &mut CudaSlice<i32>,
568 mut direction_raw: Vec<f64>,
569 g: &[f64],
570 penalized_hessian: &ndarray::Array2<f64>,
571 step_lm_delta: f64,
572 ) -> Result<Vec<f64>, String> {
573 use gam_gpu::policy::GpuDispatchPolicy;
574 if p < GpuDispatchPolicy::REFINEMENT_MIN_P {
575 return Ok(direction_raw);
576 }
577 let norm_g = g.iter().map(|v| v * v).sum::<f64>().sqrt();
578 if norm_g == 0.0 {
579 return Ok(direction_raw);
580 }
581 let hx: Vec<f64> = (0..p)
582 .map(|i| {
583 penalized_hessian
584 .row(i)
585 .iter()
586 .zip(direction_raw.iter())
587 .map(|(hij, xj)| hij * xj)
588 .sum::<f64>()
589 + step_lm_delta * direction_raw[i]
590 })
591 .collect();
592 let residual: Vec<f64> = g.iter().zip(hx.iter()).map(|(gi, hxi)| gi - hxi).collect();
593 let rel_res = residual.iter().map(|v| v * v).sum::<f64>().sqrt() / norm_g;
594 if rel_res <= GpuDispatchPolicy::REFINEMENT_TOL {
595 return Ok(direction_raw);
596 }
597 stream
598 .memcpy_htod(&residual, rhs_dev)
599 .map_err(|e| format!("upload residual: {e}"))?;
600 potrs_in_place_reuse(
601 solver,
602 stream,
603 p,
604 1,
605 chol_factor_dev,
606 rhs_dev,
607 potrs_info_dev,
608 )?;
609 let correction = stream
610 .clone_dtoh(rhs_dev)
611 .map_err(|e| format!("download correction: {e}"))?;
612 check_deferred_potrs_info(stream, potrs_info_dev)?;
613 for (xi, ei) in direction_raw.iter_mut().zip(correction.iter()) {
614 *xi += ei;
615 }
616 Ok(direction_raw)
617 }
618
619 pub(super) fn solve_step_on_stream(
630 shared: &PirlsGpuSharedData,
631 ws: &mut SigmaPirlsGpuWorkspace,
632 input: PirlsStepStreamInput<'_>,
633 ) -> Result<PirlsGpuStep, String> {
634 let n = shared.n;
635 let p = shared.p;
636 if ws.n != n || ws.p != p {
637 return Err(format!(
638 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
639 ws.n, ws.p
640 ));
641 }
642 if input.weights.len() != n {
643 return Err(format!(
644 "weights length {} does not match rows {n}",
645 input.weights.len()
646 ));
647 }
648 if input.penalty_hessian.dim() != (p, p) {
649 return Err(format!(
650 "penalty Hessian shape {:?} does not match p={p}",
651 input.penalty_hessian.dim()
652 ));
653 }
654 if input.gradient.len() != p {
655 return Err(format!(
656 "gradient length {} does not match p={p}",
657 input.gradient.len()
658 ));
659 }
660
661 let w_slice = input
663 .weights
664 .as_slice()
665 .ok_or("weights must be contiguous")?;
666 ws.stream
667 .memcpy_htod(w_slice, &mut ws.w_dev)
668 .map_err(|e| format!("upload W: {e}"))?;
669
670 let n_i = to_i32(n)?;
674 let p_i = to_i32(p)?;
675 if let Some(ref mut wx_dev) = ws.wx_dev {
676 left_scale_rows(
677 &ws.blas,
678 &ws.stream,
679 n,
680 p,
681 &shared.x_original_dev,
682 &mut ws.w_dev,
683 wx_dev,
684 )?;
685 let cfg = GemmConfig::<f64> {
686 transa: cublasOperation_t::CUBLAS_OP_T,
687 transb: cublasOperation_t::CUBLAS_OP_N,
688 m: p_i,
689 n: p_i,
690 k: n_i,
691 alpha: 1.0,
692 lda: n_i,
693 ldb: n_i,
694 beta: 0.0,
695 ldc: p_i,
696 };
697 unsafe {
700 ws.blas
701 .gemm(cfg, &shared.x_original_dev, wx_dev, &mut ws.xtwx_dev)
702 }
703 .map_err(|e| format!("cublas dgemm XtWX: {e}"))?;
704 } else {
705 launch_xtwx_lower(
706 &ws.stream,
707 &shared.ctx,
708 n,
709 p,
710 &shared.x_original_dev,
711 &ws.w_dev,
712 &mut ws.xtwx_dev,
713 )?;
714 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
715 }
716
717 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
719 let penalty_step_view = penalty_step.view();
720 let penalty_step_col = to_col_major(&penalty_step_view);
721 ws.stream
722 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
723 .map_err(|e| format!("upload penalty: {e}"))?;
724
725 {
729 let cfg_aq = GemmConfig::<f64> {
730 transa: cublasOperation_t::CUBLAS_OP_N,
731 transb: cublasOperation_t::CUBLAS_OP_N,
732 m: p_i,
733 n: p_i,
734 k: p_i,
735 alpha: 1.0,
736 lda: p_i,
737 ldb: p_i,
738 beta: 0.0,
739 ldc: p_i,
740 };
741 unsafe {
743 ws.blas
744 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
745 }
746 .map_err(|e| format!("dgemm A·Qs (host-input step): {e}"))?;
747 }
748 {
749 let cfg_qt = GemmConfig::<f64> {
750 transa: cublasOperation_t::CUBLAS_OP_T,
751 transb: cublasOperation_t::CUBLAS_OP_N,
752 m: p_i,
753 n: p_i,
754 k: p_i,
755 alpha: 1.0,
756 lda: p_i,
757 ldb: p_i,
758 beta: 0.0,
759 ldc: p_i,
760 };
761 unsafe {
763 ws.blas
764 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
765 }
766 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (host-input step): {e}"))?;
767 }
768 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
770
771 let g_slice = input
775 .gradient
776 .as_slice()
777 .ok_or("gradient must be contiguous")?;
778 ws.stream
779 .memcpy_htod(g_slice, &mut ws.rhs_dev)
780 .map_err(|e| format!("upload gradient: {e}"))?;
781
782 let xtwx_col = ws
786 .stream
787 .clone_dtoh(&ws.xtwx_dev)
788 .map_err(|e| format!("download XᵀWX (host-input step): {e}"))?;
789 let xtwx_host = from_col_major(&xtwx_col, p, p).ok_or("XᵀWX layout conversion failed")?;
790 let qs_col = ws
791 .stream
792 .clone_dtoh(&ws.qs_dev)
793 .map_err(|e| format!("download Qs (host-input step): {e}"))?;
794 let qs_host =
795 from_col_major(&qs_col, p, p).ok_or("Qs layout conversion failed (host-input step)")?;
796 let tmp_aq = xtwx_host.dot(&qs_host);
797 let h_rotated = qs_host.t().dot(&tmp_aq);
798 let penalty_export = penalty_with_ridge(input.penalty_hessian, input.objective_ridge);
799 let penalized_hessian = h_rotated + &penalty_export;
800
801 potrf_in_place_reuse(
804 &ws.solver,
805 &ws.stream,
806 p,
807 ws.potrf_lwork,
808 &mut ws.h_dev,
809 &mut ws.potrf_work_dev,
810 &mut ws.potrf_info_dev,
811 )?;
812 potrs_in_place_reuse(
813 &ws.solver,
814 &ws.stream,
815 p,
816 1,
817 &ws.h_dev,
818 &mut ws.rhs_dev,
819 &mut ws.potrs_info_dev,
820 )?;
821
822 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
826
827 let direction_raw = ws
829 .stream
830 .clone_dtoh(&ws.rhs_dev)
831 .map_err(|e| format!("download direction: {e}"))?;
832 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
836 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
837
838 let lm_ridge_delta = input.step_lm_lambda - input.objective_ridge;
842 let direction_raw = newton_step_refine_once(
843 &ws.solver,
844 &ws.stream,
845 p,
846 &ws.h_dev,
847 &mut ws.rhs_dev,
848 &mut ws.potrs_info_dev,
849 direction_raw,
850 g_slice,
851 &penalized_hessian,
852 lm_ridge_delta,
853 )?;
854
855 let direction = Array1::from_vec(direction_raw);
858
859 Ok(PirlsGpuStep {
860 penalized_hessian,
861 direction,
862 logdet,
863 })
864 }
865
866 pub(super) fn solve_step_on_stream_device(
882 shared: &PirlsGpuSharedData,
883 ws: &mut SigmaPirlsGpuWorkspace,
884 input: PirlsStepStreamDeviceInput<'_, '_>,
885 ) -> Result<PirlsGpuStep, String> {
886 let n = shared.n;
887 let p = shared.p;
888 if ws.n != n || ws.p != p {
889 return Err(format!(
890 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
891 ws.n, ws.p
892 ));
893 }
894 if input.w_solver_dev.len() != n {
895 return Err(format!(
896 "w_solver_dev length {} does not match n={n}",
897 input.w_solver_dev.len()
898 ));
899 }
900 if input.grad_eta_dev.len() != n {
901 return Err(format!(
902 "grad_eta_dev length {} does not match n={n}",
903 input.grad_eta_dev.len()
904 ));
905 }
906 if input.penalty_hessian.dim() != (p, p) {
907 return Err(format!(
908 "penalty Hessian shape {:?} does not match p={p}",
909 input.penalty_hessian.dim()
910 ));
911 }
912
913 let n_i = to_i32(n)?;
916 let p_i = to_i32(p)?;
917 if let Some(ref mut wx_dev_fb) = ws.wx_dev {
918 left_scale_rows_borrowed(
920 &ws.blas,
921 &ws.stream,
922 n,
923 p,
924 &shared.x_original_dev,
925 input.w_solver_dev,
926 wx_dev_fb,
927 )?;
928 let gemm_cfg = GemmConfig::<f64> {
929 transa: cublasOperation_t::CUBLAS_OP_T,
930 transb: cublasOperation_t::CUBLAS_OP_N,
931 m: p_i,
932 n: p_i,
933 k: n_i,
934 alpha: 1.0,
935 lda: n_i,
936 ldb: n_i,
937 beta: 0.0,
938 ldc: p_i,
939 };
940 unsafe {
943 ws.blas.gemm(
944 gemm_cfg,
945 &shared.x_original_dev,
946 wx_dev_fb,
947 &mut ws.xtwx_dev,
948 )
949 }
950 .map_err(|e| format!("cublas dgemm XtWX (device-input): {e}"))?;
951 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
952 let penalty_step_col = to_col_major(&penalty_step);
953 ws.stream
954 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
955 .map_err(|e| format!("upload penalty (device-input): {e}"))?;
956 {
958 let cfg_aq = GemmConfig::<f64> {
959 transa: cublasOperation_t::CUBLAS_OP_N,
960 transb: cublasOperation_t::CUBLAS_OP_N,
961 m: p_i,
962 n: p_i,
963 k: p_i,
964 alpha: 1.0,
965 lda: p_i,
966 ldb: p_i,
967 beta: 0.0,
968 ldc: p_i,
969 };
970 unsafe {
972 ws.blas
973 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
974 }
975 .map_err(|e| format!("dgemm A·Qs (device-input large-p): {e}"))?;
976 }
977 {
978 let cfg_qt = GemmConfig::<f64> {
979 transa: cublasOperation_t::CUBLAS_OP_T,
980 transb: cublasOperation_t::CUBLAS_OP_N,
981 m: p_i,
982 n: p_i,
983 k: p_i,
984 alpha: 1.0,
985 lda: p_i,
986 ldb: p_i,
987 beta: 0.0,
988 ldc: p_i,
989 };
990 unsafe {
992 ws.blas
993 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
994 }
995 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (device-input large-p): {e}"))?;
996 }
997 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
998 let gemv_cfg = GemvConfig::<f64> {
999 trans: cublasOperation_t::CUBLAS_OP_T,
1000 m: n_i,
1001 n: p_i,
1002 alpha: 1.0,
1003 lda: n_i,
1004 incx: 1,
1005 beta: 0.0,
1006 incy: 1,
1007 };
1008 unsafe {
1010 ws.blas.gemv(
1011 gemv_cfg,
1012 &shared.x_original_dev,
1013 input.grad_eta_dev,
1014 &mut ws.rhs_dev,
1015 )
1016 }
1017 .map_err(|e| format!("cublas dgemv Xtg (device-input): {e}"))?;
1018 } else {
1019 launch_xtwx_lower(
1021 &ws.stream,
1022 &shared.ctx,
1023 n,
1024 p,
1025 &shared.x_original_dev,
1026 input.w_solver_dev,
1027 &mut ws.xtwx_dev,
1028 )?;
1029 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1030 launch_xtscore(
1031 &ws.stream,
1032 &shared.ctx,
1033 n,
1034 p,
1035 &shared.x_original_dev,
1036 input.grad_eta_dev,
1037 &mut ws.rhs_dev,
1038 )?;
1039 {
1041 let cfg_aq = GemmConfig::<f64> {
1042 transa: cublasOperation_t::CUBLAS_OP_N,
1043 transb: cublasOperation_t::CUBLAS_OP_N,
1044 m: p_i,
1045 n: p_i,
1046 k: p_i,
1047 alpha: 1.0,
1048 lda: p_i,
1049 ldb: p_i,
1050 beta: 0.0,
1051 ldc: p_i,
1052 };
1053 unsafe {
1055 ws.blas
1056 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1057 }
1058 .map_err(|e| format!("dgemm A·Qs (device-input fused): {e}"))?;
1059 }
1060 {
1061 let cfg_qt = GemmConfig::<f64> {
1062 transa: cublasOperation_t::CUBLAS_OP_T,
1063 transb: cublasOperation_t::CUBLAS_OP_N,
1064 m: p_i,
1065 n: p_i,
1066 k: p_i,
1067 alpha: 1.0,
1068 lda: p_i,
1069 ldb: p_i,
1070 beta: 0.0,
1071 ldc: p_i,
1072 };
1073 unsafe {
1075 ws.blas
1076 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1077 }
1078 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (device-input fused): {e}"))?;
1079 }
1080 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
1081 let penalty_step_col = to_col_major(&penalty_step);
1082 ws.stream
1083 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
1084 .map_err(|e| format!("upload penalty (fused device-input): {e}"))?;
1085 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1086 }
1087
1088 {
1093 let cfg_qts = GemvConfig::<f64> {
1095 trans: cublasOperation_t::CUBLAS_OP_T,
1096 m: p_i,
1097 n: p_i,
1098 alpha: 1.0,
1099 lda: p_i,
1100 incx: 1,
1101 beta: 0.0,
1102 incy: 1,
1103 };
1104 unsafe {
1106 ws.blas
1107 .gemv(cfg_qts, &ws.qs_dev, &ws.rhs_dev, &mut ws.beta_orig_dev)
1108 }
1109 .map_err(|e| format!("dgemv Qsᵀ·score (device-input): {e}"))?;
1110 ws.stream
1112 .memcpy_dtod(&ws.beta_orig_dev, &mut ws.rhs_dev)
1113 .map_err(|e| format!("d2d Qsᵀ·score→rhs (device-input): {e}"))?;
1114 let rhs_raw = ws
1116 .stream
1117 .clone_dtoh(&ws.rhs_dev)
1118 .map_err(|e| format!("download Qsᵀscore (device-input): {e}"))?;
1119 let beta_raw = ws
1120 .stream
1121 .clone_dtoh(input.beta_dev)
1122 .map_err(|e| format!("download beta (device-input): {e}"))?;
1123 let mut rhs_host = Array1::from_vec(rhs_raw);
1124 let beta_host = Array1::from_vec(beta_raw);
1125 let s_beta = input.penalty_hessian.dot(&beta_host);
1126 rhs_host -= &s_beta;
1127 rhs_host += &input.linear_shift;
1128 ws.stream
1129 .memcpy_htod(
1130 rhs_host
1131 .as_slice()
1132 .ok_or("rhs_host not contiguous (device-input correction)")?,
1133 &mut ws.rhs_dev,
1134 )
1135 .map_err(|e| format!("re-upload corrected rhs (device-input): {e}"))?;
1136 }
1137
1138 let xtwx_col = ws
1142 .stream
1143 .clone_dtoh(&ws.xtwx_dev)
1144 .map_err(|e| format!("download XᵀWX (device-input): {e}"))?;
1145 let xtwx_host = from_col_major(&xtwx_col, p, p)
1146 .ok_or("XᵀWX layout conversion failed (device-input)")?;
1147 let qs_col = ws
1148 .stream
1149 .clone_dtoh(&ws.qs_dev)
1150 .map_err(|e| format!("download Qs (device-input): {e}"))?;
1151 let qs_host =
1152 from_col_major(&qs_col, p, p).ok_or("Qs layout conversion failed (device-input)")?;
1153 let tmp_aq = xtwx_host.dot(&qs_host);
1154 let h_rotated = qs_host.t().dot(&tmp_aq);
1155 let penalty_export = penalty_with_ridge(input.penalty_hessian, input.objective_ridge);
1156 let penalized_hessian = h_rotated + &penalty_export;
1157
1158 potrf_in_place_reuse(
1161 &ws.solver,
1162 &ws.stream,
1163 p,
1164 ws.potrf_lwork,
1165 &mut ws.h_dev,
1166 &mut ws.potrf_work_dev,
1167 &mut ws.potrf_info_dev,
1168 )?;
1169 potrs_in_place_reuse(
1170 &ws.solver,
1171 &ws.stream,
1172 p,
1173 1,
1174 &ws.h_dev,
1175 &mut ws.rhs_dev,
1176 &mut ws.potrs_info_dev,
1177 )?;
1178
1179 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
1180
1181 let direction_raw = ws
1182 .stream
1183 .clone_dtoh(&ws.rhs_dev)
1184 .map_err(|e| format!("download direction (device-input): {e}"))?;
1185 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
1189 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
1190 let direction = Array1::from_vec(direction_raw);
1193
1194 Ok(PirlsGpuStep {
1195 penalized_hessian,
1196 direction,
1197 logdet,
1198 })
1199 }
1200
1201 pub(super) fn solve_step_on_stream_device_inplace(
1212 shared: &PirlsGpuSharedData,
1213 ws: &mut SigmaPirlsGpuWorkspace,
1214 input: PirlsStepStreamDeviceInput<'_, '_>,
1215 ) -> Result<f64, String> {
1216 let n = shared.n;
1217 let p = shared.p;
1218 if ws.n != n || ws.p != p {
1219 return Err(format!(
1220 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
1221 ws.n, ws.p
1222 ));
1223 }
1224 if input.w_solver_dev.len() != n {
1225 return Err(format!(
1226 "w_solver_dev length {} does not match n={n}",
1227 input.w_solver_dev.len()
1228 ));
1229 }
1230 if input.grad_eta_dev.len() != n {
1231 return Err(format!(
1232 "grad_eta_dev length {} does not match n={n}",
1233 input.grad_eta_dev.len()
1234 ));
1235 }
1236 if input.penalty_hessian.dim() != (p, p) {
1237 return Err(format!(
1238 "penalty Hessian shape {:?} does not match p={p}",
1239 input.penalty_hessian.dim()
1240 ));
1241 }
1242
1243 if input.linear_shift.len() != p {
1244 return Err(format!(
1245 "linear_shift length {} does not match p={p}",
1246 input.linear_shift.len()
1247 ));
1248 }
1249 let n_i = to_i32(n)?;
1250 let p_i = to_i32(p)?;
1251
1252 if let Some(ref mut wx_dev_ib) = ws.wx_dev {
1255 left_scale_rows_borrowed(
1257 &ws.blas,
1258 &ws.stream,
1259 n,
1260 p,
1261 &shared.x_original_dev,
1262 input.w_solver_dev,
1263 wx_dev_ib,
1264 )?;
1265 let cfg_xtx = GemmConfig::<f64> {
1266 transa: cublasOperation_t::CUBLAS_OP_T,
1267 transb: cublasOperation_t::CUBLAS_OP_N,
1268 m: p_i,
1269 n: p_i,
1270 k: n_i,
1271 alpha: 1.0,
1272 lda: n_i,
1273 ldb: n_i,
1274 beta: 0.0,
1275 ldc: p_i,
1276 };
1277 unsafe {
1279 ws.blas
1280 .gemm(cfg_xtx, &shared.x_original_dev, wx_dev_ib, &mut ws.xtwx_dev)
1281 }
1282 .map_err(|e| format!("dgemm XtWX inplace (large-p): {e}"))?;
1283 let cfg_xts = GemvConfig::<f64> {
1284 trans: cublasOperation_t::CUBLAS_OP_T,
1285 m: n_i,
1286 n: p_i,
1287 alpha: 1.0,
1288 lda: n_i,
1289 incx: 1,
1290 beta: 0.0,
1291 incy: 1,
1292 };
1293 unsafe {
1295 ws.blas.gemv(
1296 cfg_xts,
1297 &shared.x_original_dev,
1298 input.grad_eta_dev,
1299 &mut ws.rhs_dev,
1300 )
1301 }
1302 .map_err(|e| format!("dgemv Xᵀ·score inplace (large-p): {e}"))?;
1303 } else {
1304 launch_xtwx_lower(
1306 &ws.stream,
1307 &shared.ctx,
1308 n,
1309 p,
1310 &shared.x_original_dev,
1311 input.w_solver_dev,
1312 &mut ws.xtwx_dev,
1313 )?;
1314 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1315 launch_xtscore(
1316 &ws.stream,
1317 &shared.ctx,
1318 n,
1319 p,
1320 &shared.x_original_dev,
1321 input.grad_eta_dev,
1322 &mut ws.rhs_dev,
1323 )?;
1324 }
1325
1326 {
1329 let cfg_aq = GemmConfig::<f64> {
1330 transa: cublasOperation_t::CUBLAS_OP_N,
1331 transb: cublasOperation_t::CUBLAS_OP_N,
1332 m: p_i,
1333 n: p_i,
1334 k: p_i,
1335 alpha: 1.0,
1336 lda: p_i,
1337 ldb: p_i,
1338 beta: 0.0,
1339 ldc: p_i,
1340 };
1341 unsafe {
1343 ws.blas
1344 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1345 }
1346 .map_err(|e| format!("dgemm A·Qs inplace: {e}"))?;
1347 }
1348 {
1350 let cfg_qt = GemmConfig::<f64> {
1351 transa: cublasOperation_t::CUBLAS_OP_T,
1352 transb: cublasOperation_t::CUBLAS_OP_N,
1353 m: p_i,
1354 n: p_i,
1355 k: p_i,
1356 alpha: 1.0,
1357 lda: p_i,
1358 ldb: p_i,
1359 beta: 0.0,
1360 ldc: p_i,
1361 };
1362 unsafe {
1364 ws.blas
1365 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1366 }
1367 .map_err(|e| format!("dgemm Qsᵀ·A·Qs inplace: {e}"))?;
1368 }
1369 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
1371 let penalty_step_col = to_col_major(&penalty_step);
1372 ws.stream
1373 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
1374 .map_err(|e| format!("upload penalty inplace: {e}"))?;
1375 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1376
1377 {
1381 let cfg_qts = GemvConfig::<f64> {
1382 trans: cublasOperation_t::CUBLAS_OP_T,
1383 m: p_i,
1384 n: p_i,
1385 alpha: 1.0,
1386 lda: p_i,
1387 incx: 1,
1388 beta: 0.0,
1389 incy: 1,
1390 };
1391 unsafe {
1393 ws.blas
1394 .gemv(cfg_qts, &ws.qs_dev, &ws.rhs_dev, &mut ws.beta_orig_dev)
1395 }
1396 .map_err(|e| format!("dgemv Qsᵀ·score inplace: {e}"))?;
1397 ws.stream
1398 .memcpy_dtod(&ws.beta_orig_dev, &mut ws.rhs_dev)
1399 .map_err(|e| format!("d2d Qsᵀ·score→rhs inplace: {e}"))?;
1400 }
1401 let rhs_raw = ws
1404 .stream
1405 .clone_dtoh(&ws.rhs_dev)
1406 .map_err(|e| format!("download Qsᵀ·score inplace: {e}"))?;
1407 let beta_raw = ws
1408 .stream
1409 .clone_dtoh(input.beta_dev)
1410 .map_err(|e| format!("download beta inplace: {e}"))?;
1411 let mut rhs_host = Array1::from_vec(rhs_raw);
1412 let beta_host = Array1::from_vec(beta_raw);
1413 let s_beta = input.penalty_hessian.dot(&beta_host);
1415 rhs_host -= &s_beta;
1416 rhs_host += &input.linear_shift;
1417 ws.stream
1418 .memcpy_htod(
1419 rhs_host.as_slice().ok_or("rhs_host not contiguous")?,
1420 &mut ws.rhs_dev,
1421 )
1422 .map_err(|e| format!("re-upload corrected rhs inplace: {e}"))?;
1423
1424 potrf_in_place_reuse(
1426 &ws.solver,
1427 &ws.stream,
1428 p,
1429 ws.potrf_lwork,
1430 &mut ws.h_dev,
1431 &mut ws.potrf_work_dev,
1432 &mut ws.potrf_info_dev,
1433 )?;
1434 potrs_in_place_reuse(
1435 &ws.solver,
1436 &ws.stream,
1437 p,
1438 1,
1439 &ws.h_dev,
1440 &mut ws.rhs_dev,
1441 &mut ws.potrs_info_dev,
1442 )?;
1443 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
1444 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
1445 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
1446
1447 Ok(logdet)
1450 }
1451
1452 pub(super) fn rebuild_h_final(
1460 shared: &PirlsGpuSharedData,
1461 ws: &mut SigmaPirlsGpuWorkspace,
1462 w_hessian_dev: &CudaSlice<f64>,
1463 penalty_hessian: ArrayView2<'_, f64>,
1464 objective_ridge: f64,
1465 ) -> Result<Array2<f64>, String> {
1466 let n = shared.n;
1467 let p = shared.p;
1468
1469 if let Some(ref mut wx_dev_rh) = ws.wx_dev {
1471 left_scale_rows_borrowed(
1473 &ws.blas,
1474 &ws.stream,
1475 n,
1476 p,
1477 &shared.x_original_dev,
1478 w_hessian_dev,
1479 wx_dev_rh,
1480 )?;
1481 let n_i = to_i32(n)?;
1482 let p_i = to_i32(p)?;
1483 let gemm_cfg = GemmConfig::<f64> {
1484 transa: cublasOperation_t::CUBLAS_OP_T,
1485 transb: cublasOperation_t::CUBLAS_OP_N,
1486 m: p_i,
1487 n: p_i,
1488 k: n_i,
1489 alpha: 1.0,
1490 lda: n_i,
1491 ldb: n_i,
1492 beta: 0.0,
1493 ldc: p_i,
1494 };
1495 unsafe {
1498 ws.blas.gemm(
1499 gemm_cfg,
1500 &shared.x_original_dev,
1501 wx_dev_rh,
1502 &mut ws.xtwx_dev,
1503 )
1504 }
1505 .map_err(|e| format!("cublas dgemm XtWX (final H rebuild): {e}"))?;
1506 } else {
1507 launch_xtwx_lower(
1509 &ws.stream,
1510 &shared.ctx,
1511 n,
1512 p,
1513 &shared.x_original_dev,
1514 w_hessian_dev,
1515 &mut ws.xtwx_dev,
1516 )?;
1517 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1518 }
1519
1520 let p_i = to_i32(p)?;
1522 {
1524 let cfg_aq = GemmConfig::<f64> {
1525 transa: cublasOperation_t::CUBLAS_OP_N,
1526 transb: cublasOperation_t::CUBLAS_OP_N,
1527 m: p_i,
1528 n: p_i,
1529 k: p_i,
1530 alpha: 1.0,
1531 lda: p_i,
1532 ldb: p_i,
1533 beta: 0.0,
1534 ldc: p_i,
1535 };
1536 unsafe {
1538 ws.blas
1539 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1540 }
1541 .map_err(|e| format!("dgemm A·Qs (final H rebuild): {e}"))?;
1542 }
1543 {
1545 let cfg_qt = GemmConfig::<f64> {
1546 transa: cublasOperation_t::CUBLAS_OP_T,
1547 transb: cublasOperation_t::CUBLAS_OP_N,
1548 m: p_i,
1549 n: p_i,
1550 k: p_i,
1551 alpha: 1.0,
1552 lda: p_i,
1553 ldb: p_i,
1554 beta: 0.0,
1555 ldc: p_i,
1556 };
1557 unsafe {
1559 ws.blas
1560 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1561 }
1562 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (final H rebuild): {e}"))?;
1563 }
1564 let penalty = penalty_with_ridge(penalty_hessian, objective_ridge);
1565 let penalty_col = to_col_major(&penalty);
1566 ws.stream
1567 .memcpy_htod(penalty_col.as_ref(), &mut ws.penalty_dev)
1568 .map_err(|e| format!("upload penalty (final H rebuild): {e}"))?;
1569 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1570
1571 let h_col = ws
1573 .stream
1574 .clone_dtoh(&ws.h_dev)
1575 .map_err(|e| format!("download H_final: {e}"))?;
1576 from_col_major(&h_col, p, p).ok_or_else(|| "H_final layout conversion failed".to_string())
1577 }
1578
1579 pub(super) fn weighted_crossprod(
1580 x: ArrayView2<'_, f64>,
1581 weights: ArrayView1<'_, f64>,
1582 ) -> Result<Array2<f64>, String> {
1583 let (_, stream) = context_and_stream()?;
1584 let (n, p) = validate_design(x, weights)?;
1585 let blas = CudaBlas::new(stream.clone()).map_err(|e| format!("cublas init: {e}"))?;
1586 let x_col = to_col_major(&x);
1587 let x_dev = pinned_htod(&stream, &x_col)?;
1588 let mut w_dev = pinned_htod(
1589 &stream,
1590 weights.as_slice().ok_or("weights must be contiguous")?,
1591 )?;
1592 let mut wx_dev = stream
1593 .alloc_zeros::<f64>(n.checked_mul(p).ok_or("X size overflow")?)
1594 .map_err(|e| format!("cuda alloc WX: {e}"))?;
1595 left_scale_rows(&blas, &stream, n, p, &x_dev, &mut w_dev, &mut wx_dev)?;
1596 let mut h_dev = stream
1597 .alloc_zeros::<f64>(p.checked_mul(p).ok_or("H size overflow")?)
1598 .map_err(|e| format!("cuda alloc H: {e}"))?;
1599 let n_i = to_i32(n)?;
1600 let p_i = to_i32(p)?;
1601 let cfg = GemmConfig::<f64> {
1602 transa: cublasOperation_t::CUBLAS_OP_T,
1603 transb: cublasOperation_t::CUBLAS_OP_N,
1604 m: p_i,
1605 n: p_i,
1606 k: n_i,
1607 alpha: 1.0,
1608 lda: n_i,
1609 ldb: n_i,
1610 beta: 0.0,
1611 ldc: p_i,
1612 };
1613 unsafe { blas.gemm(cfg, &x_dev, &wx_dev, &mut h_dev) }
1616 .map_err(|e| format!("cublas dgemm XtWX: {e}"))?;
1617 let h_col = stream
1618 .clone_dtoh(&h_dev)
1619 .map_err(|e| format!("download H: {e}"))?;
1620 from_col_major(&h_col, p, p).ok_or_else(|| "H layout conversion failed".to_string())
1621 }
1622
1623 pub(super) fn solve_step(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
1624 let (_, p) = validate_design(input.x, input.weights)?;
1630 if input.penalty_hessian.dim() != (p, p) {
1631 return Err(format!(
1632 "penalty Hessian shape {:?} does not match p={p}",
1633 input.penalty_hessian.dim()
1634 ));
1635 }
1636 if input.gradient.len() != p {
1637 return Err(format!(
1638 "gradient length {} does not match p={p}",
1639 input.gradient.len()
1640 ));
1641 }
1642 let n_rows = input.x.nrows();
1648 let zero_n = ndarray::Array1::<f64>::zeros(n_rows);
1649 let shared =
1650 PirlsGpuSharedData::upload_impl(input.x, zero_n.view(), zero_n.view(), zero_n.view())?;
1651 let mut ws = SigmaPirlsGpuWorkspace::allocate_impl(&shared)?;
1652 solve_step_on_stream(
1653 &shared,
1654 &mut ws,
1655 PirlsStepStreamInput {
1656 weights: input.weights,
1657 penalty_hessian: input.penalty_hessian,
1658 gradient: input.gradient,
1659 step_lm_lambda: input.step_lm_lambda,
1660 objective_ridge: input.objective_ridge,
1661 },
1662 )
1663 }
1664
1665 fn validate_design(
1666 x: ArrayView2<'_, f64>,
1667 weights: ArrayView1<'_, f64>,
1668 ) -> Result<(usize, usize), String> {
1669 let (n, p) = x.dim();
1670 if weights.len() != n {
1671 return Err(format!(
1672 "weights length {} does not match rows {n}",
1673 weights.len()
1674 ));
1675 }
1676 if n == 0 || p == 0 {
1677 return Err("empty design cannot be solved on CUDA".to_string());
1678 }
1679 Ok((n, p))
1680 }
1681
1682 fn left_scale_rows(
1683 blas: &CudaBlas,
1684 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1685 n: usize,
1686 p: usize,
1687 x_dev: &CudaSlice<f64>,
1688 w_dev: &mut CudaSlice<f64>,
1689 wx_dev: &mut CudaSlice<f64>,
1690 ) -> Result<(), String> {
1691 let n_i = to_i32(n)?;
1692 let p_i = to_i32(p)?;
1693 let handle = *blas.handle();
1694 let (x_ptr, _x_record) = x_dev.device_ptr(stream);
1695 let (w_ptr, _w_record) = w_dev.device_ptr(stream);
1696 let (wx_ptr, _wx_record) = wx_dev.device_ptr_mut(stream);
1697 let status = unsafe {
1700 cublasDdgmm(
1701 handle,
1702 cublasSideMode_t::CUBLAS_SIDE_LEFT,
1703 n_i,
1704 p_i,
1705 x_ptr as *const f64,
1706 n_i,
1707 w_ptr as *const f64,
1708 1,
1709 wx_ptr as *mut f64,
1710 n_i,
1711 )
1712 };
1713 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1714 Ok(())
1715 } else {
1716 Err(format!("cublasDdgmm failed with {status:?}"))
1717 }
1718 }
1719
1720 fn left_scale_rows_borrowed(
1725 blas: &CudaBlas,
1726 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1727 n: usize,
1728 p: usize,
1729 x_dev: &CudaSlice<f64>,
1730 w_dev: &CudaSlice<f64>,
1731 wx_dev: &mut CudaSlice<f64>,
1732 ) -> Result<(), String> {
1733 let n_i = to_i32(n)?;
1734 let p_i = to_i32(p)?;
1735 let handle = *blas.handle();
1736 let (x_ptr, _x_record) = x_dev.device_ptr(stream);
1737 let (w_ptr, _w_record) = w_dev.device_ptr(stream);
1738 let (wx_ptr, _wx_record) = wx_dev.device_ptr_mut(stream);
1739 let status = unsafe {
1744 cublasDdgmm(
1745 handle,
1746 cublasSideMode_t::CUBLAS_SIDE_LEFT,
1747 n_i,
1748 p_i,
1749 x_ptr as *const f64,
1750 n_i,
1751 w_ptr as *const f64,
1752 1,
1753 wx_ptr as *mut f64,
1754 n_i,
1755 )
1756 };
1757 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1758 Ok(())
1759 } else {
1760 Err(format!("cublasDdgmm (borrowed) failed with {status:?}"))
1761 }
1762 }
1763
1764 fn geam_add_inplace(
1772 blas: &CudaBlas,
1773 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1774 p: usize,
1775 a: &mut CudaSlice<f64>,
1776 b: &CudaSlice<f64>,
1777 ) -> Result<(), String> {
1778 let p_i = to_i32(p)?;
1779 let alpha = 1.0_f64;
1780 let beta = 1.0_f64;
1781 let handle = *blas.handle();
1782 let (b_ptr, _b_record) = b.device_ptr(stream);
1783 let (a_ptr, _a_record) = a.device_ptr_mut(stream);
1784 let out_ptr = a_ptr;
1786 let status = unsafe {
1789 cublasDgeam(
1790 handle,
1791 cublasOperation_t::CUBLAS_OP_N,
1792 cublasOperation_t::CUBLAS_OP_N,
1793 p_i,
1794 p_i,
1795 &alpha,
1796 a_ptr as *const f64,
1797 p_i,
1798 &beta,
1799 b_ptr as *const f64,
1800 p_i,
1801 out_ptr as *mut f64,
1802 p_i,
1803 )
1804 };
1805 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1806 Ok(())
1807 } else {
1808 Err(format!("cublasDgeam failed with {status:?}"))
1809 }
1810 }
1811
1812 fn launch_xtwx_lower(
1816 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1817 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1818 n: usize,
1819 p: usize,
1820 x_dev: &CudaSlice<f64>,
1821 w_dev: &CudaSlice<f64>,
1822 a_dev: &mut CudaSlice<f64>,
1823 ) -> Result<(), String> {
1824 let module = FUSED_XTWX_CACHE
1825 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1826 .map_err(|e| format!("fused_xtwx module: {e}"))?;
1827 let func = module
1828 .load_function("xtwx_lower")
1829 .map_err(|e| format!("load xtwx_lower: {e}"))?;
1830 let n_i = to_i32(n)?;
1831 let p_i = to_i32(p)?;
1832 let num_pairs = p * (p + 1) / 2;
1833 let num_pairs_u32 = u32::try_from(num_pairs)
1834 .map_err(|_| format!("xtwx_lower: num_pairs {num_pairs} > u32"))?;
1835 const BLOCK: u32 = 256;
1836 let grid = num_pairs_u32.div_ceil(BLOCK).max(1);
1837 let cfg = cudarc::driver::LaunchConfig {
1838 grid_dim: (grid, 1, 1),
1839 block_dim: (BLOCK, 1, 1),
1840 shared_mem_bytes: 0,
1841 };
1842 let mut builder = stream.launch_builder(&func);
1843 builder.arg(x_dev);
1844 builder.arg(w_dev);
1845 builder.arg(a_dev);
1846 builder.arg(&n_i);
1847 builder.arg(&p_i);
1848 unsafe { builder.launch(cfg) }
1851 .map_err(|e| format!("xtwx_lower launch: {e}"))
1852 .map(|_| ())
1853 }
1854
1855 fn launch_xtscore(
1858 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1859 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1860 n: usize,
1861 p: usize,
1862 x_dev: &CudaSlice<f64>,
1863 score_dev: &CudaSlice<f64>,
1864 s_dev: &mut CudaSlice<f64>,
1865 ) -> Result<(), String> {
1866 let module = FUSED_XTWX_CACHE
1867 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1868 .map_err(|e| format!("fused_xtwx module (xtscore): {e}"))?;
1869 let func = module
1870 .load_function("xtscore")
1871 .map_err(|e| format!("load xtscore: {e}"))?;
1872 let n_i = to_i32(n)?;
1873 let p_i = to_i32(p)?;
1874 let p_u32 = u32::try_from(p).map_err(|_| format!("xtscore: p {p} > u32"))?;
1875 const BLOCK: u32 = 256;
1876 let grid = p_u32.div_ceil(BLOCK).max(1);
1877 let cfg = cudarc::driver::LaunchConfig {
1878 grid_dim: (grid, 1, 1),
1879 block_dim: (BLOCK, 1, 1),
1880 shared_mem_bytes: 0,
1881 };
1882 let mut builder = stream.launch_builder(&func);
1883 builder.arg(x_dev);
1884 builder.arg(score_dev);
1885 builder.arg(s_dev);
1886 builder.arg(&n_i);
1887 builder.arg(&p_i);
1888 unsafe { builder.launch(cfg) }
1891 .map_err(|e| format!("xtscore launch: {e}"))
1892 .map(|_| ())
1893 }
1894
1895 fn launch_symmetrize_lower(
1899 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1900 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1901 p: usize,
1902 a_dev: &mut CudaSlice<f64>,
1903 ) -> Result<(), String> {
1904 if p <= 1 {
1905 return Ok(());
1906 }
1907 let module = FUSED_XTWX_CACHE
1908 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1909 .map_err(|e| format!("fused_xtwx module (sym): {e}"))?;
1910 let func = module
1911 .load_function("symmetrize_lower")
1912 .map_err(|e| format!("load symmetrize_lower: {e}"))?;
1913 let p_i = to_i32(p)?;
1914 let num_strict = p * (p - 1) / 2;
1915 let num_strict_u32 = u32::try_from(num_strict)
1916 .map_err(|_| format!("symmetrize_lower: num_strict {num_strict} > u32"))?;
1917 const BLOCK: u32 = 256;
1918 let grid = num_strict_u32.div_ceil(BLOCK).max(1);
1919 let cfg = cudarc::driver::LaunchConfig {
1920 grid_dim: (grid, 1, 1),
1921 block_dim: (BLOCK, 1, 1),
1922 shared_mem_bytes: 0,
1923 };
1924 let mut builder = stream.launch_builder(&func);
1925 builder.arg(a_dev);
1926 builder.arg(&p_i);
1927 unsafe { builder.launch(cfg) }
1930 .map_err(|e| format!("symmetrize_lower launch: {e}"))
1931 .map(|_| ())
1932 }
1933
1934 fn cholesky_logdet_device(
1939 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1940 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1941 p: usize,
1942 factor_dev: &CudaSlice<f64>,
1943 ) -> Result<f64, String> {
1944 let module = CHOL_LOGDET_CACHE
1945 .get_or_compile(ctx, "pirls_gpu_chol_logdet", CHOL_LOGDET_PTX_SOURCE)
1946 .map_err(|err| format!("chol_logdet module: {err}"))?;
1947 let func = module
1948 .load_function("chol_logdet_col_major")
1949 .map_err(|err| format!("chol_logdet load_function: {err}"))?;
1950 let mut out_dev = stream
1951 .alloc_zeros::<f64>(1)
1952 .map_err(|err| format!("alloc chol_logdet out: {err}"))?;
1953 let p_i = to_i32(p)?;
1954 let cfg = LaunchConfig {
1955 grid_dim: (1, 1, 1),
1956 block_dim: (1, 1, 1),
1957 shared_mem_bytes: 0,
1958 };
1959 let mut builder = stream.launch_builder(&func);
1960 builder.arg(factor_dev);
1961 builder.arg(&p_i);
1962 builder.arg(&mut out_dev);
1963 unsafe { builder.launch(cfg) }.map_err(|err| format!("chol_logdet launch: {err}"))?;
1968 let out_host = stream
1969 .clone_dtoh(&out_dev)
1970 .map_err(|err| format!("download chol_logdet: {err}"))?;
1971 Ok(out_host[0])
1972 }
1973
1974 fn penalty_with_ridge(penalty: ArrayView2<'_, f64>, ridge: f64) -> Array2<f64> {
1975 let mut out = penalty.to_owned();
1976 if ridge != 0.0 {
1977 for i in 0..out.nrows().min(out.ncols()) {
1978 out[[i, i]] += ridge;
1979 }
1980 }
1981 out
1982 }
1983
1984 fn to_i32(value: usize) -> Result<i32, String> {
1985 i32::try_from(value).map_err(|_| format!("CUDA dimension {value} exceeds i32"))
1986 }
1987
1988 const PIRLS_LOOP_PTX_SOURCE: &str = r#"
1995// __device__ annotation required by newer NVRTC JIT semantics (see
1996// gpu_kernels/pirls_row.rs common_device_prolog — the #2313 hardware sweep).
1997extern "C" {
1998 __device__ double fabs(double);
1999}
2000
2001extern "C" __global__ void axpy_n(
2002 double alpha,
2003 const double* __restrict__ x,
2004 double* __restrict__ y,
2005 int n
2006) {
2007 int i = blockIdx.x * blockDim.x + threadIdx.x;
2008 if (i >= n) return;
2009 y[i] += alpha * x[i];
2010}
2011
2012extern "C" __global__ void deviance_sum(
2013 const double* __restrict__ d,
2014 int n,
2015 double* __restrict__ out
2016) {
2017 __shared__ double sm[1024];
2018 int tid = threadIdx.x;
2019 int bdim = blockDim.x;
2020 double acc = 0.0;
2021 for (int i = tid; i < n; i += bdim) {
2022 acc += d[i];
2023 }
2024 sm[tid] = acc;
2025 __syncthreads();
2026 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2027 if (tid < stride) sm[tid] += sm[tid + stride];
2028 __syncthreads();
2029 }
2030 if (tid == 0) out[0] = sm[0];
2031}
2032
2033extern "C" __global__ void linf_norm(
2034 const double* __restrict__ v,
2035 int p,
2036 double* __restrict__ out
2037) {
2038 __shared__ double sm[1024];
2039 int tid = threadIdx.x;
2040 int bdim = blockDim.x;
2041 double acc = 0.0;
2042 for (int i = tid; i < p; i += bdim) {
2043 double a = fabs(v[i]);
2044 if (a > acc) acc = a;
2045 }
2046 sm[tid] = acc;
2047 __syncthreads();
2048 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2049 if (tid < stride) {
2050 double r = sm[tid + stride];
2051 if (r > sm[tid]) sm[tid] = r;
2052 }
2053 __syncthreads();
2054 }
2055 if (tid == 0) out[0] = sm[0];
2056}
2057
2058extern "C" __global__ void negate_n(
2059 double* __restrict__ v,
2060 int n
2061) {
2062 int i = blockIdx.x * blockDim.x + threadIdx.x;
2063 if (i >= n) return;
2064 v[i] = -v[i];
2065}
2066
2067// Deterministically select the smallest failing row. out[0] is UINT_MAX on
2068// success, otherwise the row index; out[1] carries that row's refusal code.
2069extern "C" __global__ void status_first(
2070 const unsigned int* __restrict__ status,
2071 int n,
2072 unsigned int* __restrict__ out
2073) {
2074 __shared__ unsigned int sm_row[1024];
2075 __shared__ unsigned int sm_code[1024];
2076 int tid = threadIdx.x;
2077 int bdim = blockDim.x;
2078 unsigned int best_row = 0xffffffffu;
2079 unsigned int best_code = 0u;
2080 for (int i = tid; i < n; i += bdim) {
2081 unsigned int code = status[i];
2082 if (code != 0u && (unsigned int)i < best_row) {
2083 best_row = (unsigned int)i;
2084 best_code = code;
2085 }
2086 }
2087 sm_row[tid] = best_row;
2088 sm_code[tid] = best_code;
2089 __syncthreads();
2090 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2091 if (tid < stride && sm_row[tid + stride] < sm_row[tid]) {
2092 sm_row[tid] = sm_row[tid + stride];
2093 sm_code[tid] = sm_code[tid + stride];
2094 }
2095 __syncthreads();
2096 }
2097 if (tid == 0) {
2098 out[0] = sm_row[0];
2099 out[1] = sm_code[0];
2100 }
2101}
2102
2103// Same deterministic reduction for the alpha-major [7*n] ladder status
2104// matrix. One block handles each alpha; outputs are row[0..7), code[7..14).
2105extern "C" __global__ void status_first_ladder(
2106 const unsigned int* __restrict__ status,
2107 int n,
2108 unsigned int* __restrict__ out
2109) {
2110 __shared__ unsigned int sm_row[1024];
2111 __shared__ unsigned int sm_code[1024];
2112 int k = blockIdx.x;
2113 int tid = threadIdx.x;
2114 int bdim = blockDim.x;
2115 unsigned int best_row = 0xffffffffu;
2116 unsigned int best_code = 0u;
2117 const unsigned int* candidate = status + ((long long)k * n);
2118 for (int i = tid; i < n; i += bdim) {
2119 unsigned int code = candidate[i];
2120 if (code != 0u && (unsigned int)i < best_row) {
2121 best_row = (unsigned int)i;
2122 best_code = code;
2123 }
2124 }
2125 sm_row[tid] = best_row;
2126 sm_code[tid] = best_code;
2127 __syncthreads();
2128 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2129 if (tid < stride && sm_row[tid + stride] < sm_row[tid]) {
2130 sm_row[tid] = sm_row[tid + stride];
2131 sm_code[tid] = sm_code[tid + stride];
2132 }
2133 __syncthreads();
2134 }
2135 if (tid == 0) {
2136 out[k] = sm_row[0];
2137 out[7 + k] = sm_code[0];
2138 }
2139}
2140"#;
2141
2142 static PIRLS_LOOP_CACHE: PtxModuleCache = PtxModuleCache::new();
2143
2144 pub struct PirlsLoopWorkspace {
2151 pub beta_dev: CudaSlice<f64>,
2152 pub eta_dev: CudaSlice<f64>,
2153 pub row_solve: crate::gpu_kernels::pirls_row::SolveRowBuffers,
2155 pub alpha_ladder: crate::gpu_kernels::pirls_row::AlphaLadderDevBuffers,
2157 pub row_final: crate::gpu_kernels::pirls_row::RowOutputDevBuffers,
2159 pub direction_dev: CudaSlice<f64>,
2160 pub xd_dev: CudaSlice<f64>,
2161 pub scalar_dev: CudaSlice<f64>,
2162 pub status_u32_dev: CudaSlice<u32>,
2165 pub n: usize,
2166 pub p: usize,
2167 }
2168
2169 impl PirlsLoopWorkspace {
2170 pub fn allocate(
2171 shared: &PirlsGpuSharedData,
2172 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
2173 ) -> Result<Self, String> {
2174 let n = shared.n;
2175 let p = shared.p;
2176 let alloc_f64 = |label: &'static str, len: usize| {
2177 stream
2178 .alloc_zeros::<f64>(len)
2179 .map_err(|e| format!("pirls loop alloc {label}: {e}"))
2180 };
2181 Ok(Self {
2182 beta_dev: alloc_f64("beta", p)?,
2183 eta_dev: alloc_f64("eta", n)?,
2184 row_solve: crate::gpu_kernels::pirls_row::SolveRowBuffers::allocate(stream, n)
2185 .map_err(|e| format!("pirls loop alloc row_solve: {e}"))?,
2186 alpha_ladder: crate::gpu_kernels::pirls_row::AlphaLadderDevBuffers::allocate(
2187 stream, n,
2188 )
2189 .map_err(|e| format!("pirls loop alloc alpha_ladder: {e}"))?,
2190 row_final: crate::gpu_kernels::pirls_row::RowOutputDevBuffers::allocate(stream, n)
2191 .map_err(|e| format!("pirls loop alloc row_final: {e}"))?,
2192 direction_dev: alloc_f64("direction", p)?,
2193 xd_dev: alloc_f64("xd", n)?,
2194 scalar_dev: alloc_f64("scalar", 1)?,
2195 status_u32_dev: stream
2196 .alloc_zeros::<u32>(14)
2197 .map_err(|e| format!("pirls loop alloc status_u32: {e}"))?,
2198 n,
2199 p,
2200 })
2201 }
2202 }
2203
2204 pub struct PirlsLoopExtra<'a> {
2228 pub likelihood: &'a gam_problem::GlmLikelihoodSpec,
2233 pub inverse_link: &'a gam_problem::InverseLink,
2236 pub y: ndarray::ArrayView1<'a, f64>,
2239 pub priorweights: ndarray::ArrayView1<'a, f64>,
2242 pub offset: ndarray::ArrayView1<'a, f64>,
2246 pub linear_constraints: Option<&'a gam_problem::LinearInequalityConstraints>,
2253 pub exported_curvature: crate::pirls::HessianCurvatureKind,
2263 pub ridge_passport: Option<gam_problem::RidgePassport>,
2270 pub firth: Option<crate::pirls::FirthDiagnostics>,
2276 pub edf: Option<f64>,
2285 }
2286
2287 #[derive(Clone, Debug)]
2288 pub struct PirlsLoopOutcome {
2289 pub beta: Array1<f64>,
2290 pub penalized_hessian: Array2<f64>,
2291 pub logdet: f64,
2292 pub deviance: f64,
2293 pub iterations: usize,
2294 pub converged: bool,
2295 pub final_eta: Array1<f64>,
2298 pub final_mu: Array1<f64>,
2301 pub final_grad_eta: Array1<f64>,
2305 pub final_w_hessian: Array1<f64>,
2309 pub final_w_solver: Array1<f64>,
2312 pub final_offset: Array1<f64>,
2316 pub beta_transformed: Array1<f64>,
2321 pub finalweights: Array1<f64>,
2325 pub solveweights: Array1<f64>,
2328 pub solve_dmu_deta: Array1<f64>,
2332 pub solve_d2mu_deta2: Array1<f64>,
2334 pub solve_d3mu_deta3: Array1<f64>,
2336 pub solve_c_array: Array1<f64>,
2340 pub solve_d_array: Array1<f64>,
2343 pub derivatives_unsupported: bool,
2347 pub status: crate::pirls::PirlsStatus,
2353 pub ridge_passport: gam_problem::RidgePassport,
2358 pub firth: crate::pirls::FirthDiagnostics,
2361 pub constraint_kkt: Option<crate::active_set::ConstraintKktDiagnostics>,
2365 pub edf: f64,
2368 pub last_deviance_change: f64,
2372 pub last_step_halving: usize,
2378 pub last_step_size: f64,
2381 pub final_lm_lambda: f64,
2387 pub min_deviance: f64,
2394 pub max_abs_eta: f64,
2399 }
2400
2401 pub(super) fn pirls_loop(
2405 shared: &PirlsGpuSharedData,
2406 ws: &mut SigmaPirlsGpuWorkspace,
2407 loop_ws: &mut PirlsLoopWorkspace,
2408 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
2409 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
2410 gamma_shape: f64,
2413 beta0_host: ArrayView1<'_, f64>,
2414 penalty_hessian: ArrayView2<'_, f64>,
2415 linear_shift: ArrayView1<'_, f64>,
2420 constant_shift: f64,
2423 lm_ridge: f64,
2426 objective_ridge: f64,
2429 max_iter: usize,
2430 tol: f64,
2431 extra: Option<&PirlsLoopExtra<'_>>,
2432 ) -> Result<PirlsLoopOutcome, PirlsGpuLoopError> {
2433 let n = shared.n;
2434 let p = shared.p;
2435 if loop_ws.n != n || loop_ws.p != p {
2436 return Err(format!(
2437 "loop workspace ({}, {}) ≠ shared ({n}, {p})",
2438 loop_ws.n, loop_ws.p
2439 )
2440 .into());
2441 }
2442 if beta0_host.len() != p {
2443 return Err(format!("beta0 length {} ≠ p={p}", beta0_host.len()).into());
2444 }
2445
2446 if linear_shift.len() != p {
2447 return Err(format!("linear_shift length {} ≠ p={p}", linear_shift.len()).into());
2448 }
2449 if penalty_hessian.dim() != (p, p) {
2450 return Err(format!(
2451 "penalty_hessian shape {:?} ≠ (p={p}, p={p})",
2452 penalty_hessian.dim()
2453 )
2454 .into());
2455 }
2456
2457 ws.stream
2458 .memcpy_htod(
2459 beta0_host.as_slice().ok_or("beta0 not contiguous")?,
2460 &mut loop_ws.beta_dev,
2461 )
2462 .map_err(|e| format!("upload beta0: {e}"))?;
2463
2464 let backend = crate::gpu_kernels::pirls_row::PirlsRowBackend::probe()
2465 .map_err(|e| format!("pirls_row backend: {e}"))?;
2466 let loop_module = PIRLS_LOOP_CACHE
2467 .get_or_compile(&shared.ctx, "pirls_loop", PIRLS_LOOP_PTX_SOURCE)
2468 .map_err(|e| format!("pirls loop module: {e}"))?;
2469 let axpy_func = loop_module
2470 .load_function("axpy_n")
2471 .map_err(|e| format!("load axpy_n: {e}"))?;
2472 let sum_func = loop_module
2473 .load_function("deviance_sum")
2474 .map_err(|e| format!("load deviance_sum: {e}"))?;
2475 let linf_func = loop_module
2476 .load_function("linf_norm")
2477 .map_err(|e| format!("load linf_norm: {e}"))?;
2478 let status_first_func = loop_module
2479 .load_function("status_first")
2480 .map_err(|e| format!("load status_first: {e}"))?;
2481 let status_first_ladder_func = loop_module
2482 .load_function("status_first_ladder")
2483 .map_err(|e| format!("load status_first_ladder: {e}"))?;
2484
2485 gemv_no_trans(
2488 &ws.blas,
2489 p,
2490 p,
2491 &ws.qs_dev,
2492 &loop_ws.beta_dev,
2493 &mut ws.beta_orig_dev,
2494 )?;
2495 gemv_no_trans(
2497 &ws.blas,
2498 n,
2499 p,
2500 &shared.x_original_dev,
2501 &ws.beta_orig_dev,
2502 &mut loop_ws.eta_dev,
2503 )?;
2504 axpy(
2505 &ws.stream,
2506 &axpy_func,
2507 1.0,
2508 &shared.offset_dev,
2509 &mut loop_ws.eta_dev,
2510 n,
2511 )?;
2512 crate::gpu_kernels::pirls_row::launch_solve_row_on_stream(
2514 backend,
2515 family,
2516 curvature,
2517 gamma_shape,
2518 &ws.stream,
2519 n,
2520 &loop_ws.eta_dev,
2521 &shared.y_dev,
2522 &shared.prior_w_dev,
2523 &mut loop_ws.row_solve,
2524 )
2525 .map_err(|e| format!("solve-row init: {e}"))?;
2526 certify_device_rows(
2527 &ws.stream,
2528 &status_first_func,
2529 &loop_ws.row_solve.status,
2530 &mut loop_ws.status_u32_dev,
2531 family,
2532 curvature,
2533 gamma_shape,
2534 &loop_ws.eta_dev,
2535 &shared.y_dev,
2536 &shared.prior_w_dev,
2537 n,
2538 "solve-row init",
2539 )?;
2540
2541 let mut prev_deviance = reduce_scalar(
2542 &ws.stream,
2543 &sum_func,
2544 &loop_ws.row_solve.deviance,
2545 n,
2546 &mut loop_ws.scalar_dev,
2547 "deviance_init",
2548 )?;
2549 let mut last_logdet = 0.0_f64;
2550 let mut converged = false;
2551
2552 let mut beta_host: Array1<f64> = beta0_host.to_owned();
2558
2559 let s_beta0 = penalty_hessian.dot(&beta_host);
2564 let penalty_init =
2565 beta_host.dot(&s_beta0) - 2.0 * beta_host.dot(&linear_shift) + constant_shift;
2566 let mut prev_objective = prev_deviance + penalty_init;
2567
2568 let mut last_dev_delta = 0.0_f64;
2576 let mut last_halving: usize = 0;
2577 let mut last_step_size = 0.0_f64;
2578 let mut min_dev = prev_deviance;
2579 let mut step_search_exhausted = false;
2580
2581 for it in 0..max_iter {
2582 last_logdet = solve_step_on_stream_device_inplace(
2583 shared,
2584 ws,
2585 PirlsStepStreamDeviceInput {
2586 w_solver_dev: &loop_ws.row_solve.w_solver,
2587 grad_eta_dev: &loop_ws.row_solve.grad_eta,
2588 penalty_hessian,
2589 step_lm_lambda: lm_ridge,
2590 objective_ridge,
2591 beta_dev: &loop_ws.beta_dev,
2592 linear_shift,
2593 },
2594 )
2595 .map_err(|e| format!("inner step it={it}: {e}"))?;
2596 ws.stream
2599 .memcpy_dtod(&ws.rhs_dev, &mut loop_ws.direction_dev)
2600 .map_err(|e| format!("direction d2d copy it={it}: {e}"))?;
2601
2602 let dir_linf = reduce_scalar(
2603 &ws.stream,
2604 &linf_func,
2605 &loop_ws.direction_dev,
2606 p,
2607 &mut loop_ws.scalar_dev,
2608 "dir_linf",
2609 )?;
2610
2611 gemv_no_trans(
2613 &ws.blas,
2614 p,
2615 p,
2616 &ws.qs_dev,
2617 &loop_ws.direction_dev,
2618 &mut ws.dir_orig_dev,
2619 )?;
2620 gemv_no_trans(
2621 &ws.blas,
2622 n,
2623 p,
2624 &shared.x_original_dev,
2625 &ws.dir_orig_dev,
2626 &mut loop_ws.xd_dev,
2627 )?;
2628
2629 loop_ws
2638 .alpha_ladder
2639 .zero(&ws.stream)
2640 .map_err(|e| format!("ladder zero it={it}: {e}"))?;
2641 crate::gpu_kernels::pirls_row::launch_alpha_ladder_on_stream(
2642 backend,
2643 family,
2644 curvature,
2645 gamma_shape,
2646 &ws.stream,
2647 n,
2648 &loop_ws.eta_dev,
2649 &loop_ws.xd_dev,
2650 &shared.y_dev,
2651 &shared.prior_w_dev,
2652 &mut loop_ws.alpha_ladder,
2653 )
2654 .map_err(|e| format!("alpha-ladder it={it}: {e}"))?;
2655 let obj_host: Vec<f64> = ws
2656 .stream
2657 .clone_dtoh(&loop_ws.alpha_ladder.objective_dev)
2658 .map_err(|e| format!("ladder dtoh obj it={it}: {e}"))?;
2659 let candidate_refusals = reduce_ladder_status_first(
2660 &ws.stream,
2661 &status_first_ladder_func,
2662 &loop_ws.alpha_ladder.status_dev,
2663 n,
2664 &mut loop_ws.status_u32_dev,
2665 )?;
2666 let direction_host: Vec<f64> = ws
2669 .stream
2670 .clone_dtoh(&loop_ws.direction_dev)
2671 .map_err(|e| format!("dtoh direction it={it}: {e}"))?;
2672
2673 let dir_view = ndarray::aview1(&direction_host);
2682 let sd = penalty_hessian.dot(&dir_view);
2683 let s_beta = penalty_hessian.dot(&beta_host);
2684 let dtsd = dir_view.dot(&sd);
2685 let linear_coeff = 2.0 * dir_view.dot(&(&s_beta - &linear_shift));
2686 let penalty_beta =
2687 beta_host.dot(&s_beta) - 2.0 * beta_host.dot(&linear_shift) + constant_shift;
2688
2689 let mut alpha = 0.0_f64;
2690 let mut accepted_dev = prev_deviance;
2691 let mut accepted_objective = prev_objective;
2692 let mut halving_count: usize = 0;
2693 for (k, &dev_k) in obj_host.iter().enumerate() {
2694 let a = crate::gpu_kernels::pirls_row::ALPHA_LADDER[k];
2695 let pen_k = penalty_beta + a * linear_coeff + a * a * dtsd;
2696 let obj_k = dev_k + pen_k;
2697 if candidate_refusals[k].is_none() && obj_k.is_finite() && obj_k <= prev_objective {
2705 alpha = a;
2706 accepted_dev = dev_k;
2707 accepted_objective = obj_k;
2708 halving_count = k;
2709 break;
2710 }
2711 }
2712 if alpha == 0.0 {
2713 if candidate_refusals.iter().all(Option::is_some) {
2714 let (row, code) = candidate_refusals[0]
2715 .expect("all alpha-ladder candidates were certified as refusals");
2716 let eta_host = ws
2717 .stream
2718 .clone_dtoh(&loop_ws.eta_dev)
2719 .map_err(|error| format!("ladder refusal eta download: {error}"))?;
2720 let xd_host = ws
2721 .stream
2722 .clone_dtoh(&loop_ws.xd_dev)
2723 .map_err(|error| format!("ladder refusal direction download: {error}"))?;
2724 let y_host = ws
2725 .stream
2726 .clone_dtoh(&shared.y_dev)
2727 .map_err(|error| format!("ladder refusal response download: {error}"))?;
2728 let prior_host =
2729 ws.stream.clone_dtoh(&shared.prior_w_dev).map_err(|error| {
2730 format!("ladder refusal prior-weight download: {error}")
2731 })?;
2732 let trial_eta = eta_host[row]
2733 + crate::gpu_kernels::pirls_row::ALPHA_LADDER[0] * xd_host[row];
2734 return Err(replay_row_refusal(
2735 family,
2736 curvature,
2737 gamma_shape,
2738 row,
2739 code,
2740 trial_eta,
2741 y_host[row],
2742 prior_host[row],
2743 ));
2744 }
2745 step_search_exhausted = true;
2764 last_halving = 0;
2765 last_step_size = 0.0;
2766 last_dev_delta = 0.0;
2767 break;
2768 }
2769 step_search_exhausted = false;
2770 axpy(
2772 &ws.stream,
2773 &axpy_func,
2774 alpha,
2775 &loop_ws.direction_dev,
2776 &mut loop_ws.beta_dev,
2777 p,
2778 )?;
2779 axpy(
2780 &ws.stream,
2781 &axpy_func,
2782 alpha,
2783 &loop_ws.xd_dev,
2784 &mut loop_ws.eta_dev,
2785 n,
2786 )?;
2787 for (b, &d) in beta_host.iter_mut().zip(direction_host.iter()) {
2789 *b += alpha * d;
2790 }
2791 crate::gpu_kernels::pirls_row::launch_solve_row_on_stream(
2793 backend,
2794 family,
2795 curvature,
2796 gamma_shape,
2797 &ws.stream,
2798 n,
2799 &loop_ws.eta_dev,
2800 &shared.y_dev,
2801 &shared.prior_w_dev,
2802 &mut loop_ws.row_solve,
2803 )
2804 .map_err(|e| format!("solve-row accepted it={it}: {e}"))?;
2805 certify_device_rows(
2806 &ws.stream,
2807 &status_first_func,
2808 &loop_ws.row_solve.status,
2809 &mut loop_ws.status_u32_dev,
2810 family,
2811 curvature,
2812 gamma_shape,
2813 &loop_ws.eta_dev,
2814 &shared.y_dev,
2815 &shared.prior_w_dev,
2816 n,
2817 "solve-row accepted",
2818 )?;
2819
2820 let step_norm = alpha.abs() * dir_linf;
2821 let dev_delta = (prev_objective - accepted_objective).abs();
2822 last_dev_delta = dev_delta;
2823 last_halving = halving_count;
2824 last_step_size = alpha;
2825 if accepted_dev < min_dev {
2826 min_dev = accepted_dev;
2827 }
2828
2829 prev_deviance = accepted_dev;
2830 prev_objective = accepted_objective;
2831
2832 if dir_linf <= tol
2833 && step_norm <= tol
2834 && dev_delta <= tol * (1.0 + prev_objective.abs())
2835 {
2836 converged = true;
2837 crate::gpu_kernels::pirls_row::launch_row_reweight_on_stream(
2839 backend,
2840 family,
2841 curvature,
2842 gamma_shape,
2843 &ws.stream,
2844 n,
2845 &loop_ws.eta_dev,
2846 &shared.y_dev,
2847 &shared.prior_w_dev,
2848 &mut loop_ws.row_final,
2849 )
2850 .map_err(|e| format!("final-row converged: {e}"))?;
2851 certify_device_rows(
2852 &ws.stream,
2853 &status_first_func,
2854 &loop_ws.row_final.status,
2855 &mut loop_ws.status_u32_dev,
2856 family,
2857 curvature,
2858 gamma_shape,
2859 &loop_ws.eta_dev,
2860 &shared.y_dev,
2861 &shared.prior_w_dev,
2862 n,
2863 "final-row converged",
2864 )?;
2865 let h_final = rebuild_h_final(
2866 shared,
2867 ws,
2868 &loop_ws.row_final.w_hessian,
2869 penalty_hessian,
2870 objective_ridge,
2871 )
2872 .map_err(|e| format!("rebuild H_final (converged): {e}"))?;
2873 return build_loop_outcome(
2874 ws,
2875 loop_ws,
2876 h_final,
2877 last_logdet,
2878 prev_deviance,
2879 it + 1,
2880 converged,
2881 lm_ridge,
2882 objective_ridge,
2883 extra,
2884 LoopDiagnostics {
2885 last_deviance_change: last_dev_delta,
2886 last_step_halving: last_halving,
2887 last_step_size,
2888 min_deviance: min_dev,
2889 step_search_exhausted,
2890 },
2891 );
2892 }
2893 }
2894
2895 crate::gpu_kernels::pirls_row::launch_row_reweight_on_stream(
2897 backend,
2898 family,
2899 curvature,
2900 gamma_shape,
2901 &ws.stream,
2902 n,
2903 &loop_ws.eta_dev,
2904 &shared.y_dev,
2905 &shared.prior_w_dev,
2906 &mut loop_ws.row_final,
2907 )
2908 .map_err(|e| format!("final-row max_iter: {e}"))?;
2909 certify_device_rows(
2910 &ws.stream,
2911 &status_first_func,
2912 &loop_ws.row_final.status,
2913 &mut loop_ws.status_u32_dev,
2914 family,
2915 curvature,
2916 gamma_shape,
2917 &loop_ws.eta_dev,
2918 &shared.y_dev,
2919 &shared.prior_w_dev,
2920 n,
2921 "final-row max_iter",
2922 )?;
2923 let h_final = rebuild_h_final(
2924 shared,
2925 ws,
2926 &loop_ws.row_final.w_hessian,
2927 penalty_hessian,
2928 objective_ridge,
2929 )
2930 .map_err(|e| format!("rebuild H_final (max_iter): {e}"))?;
2931 build_loop_outcome(
2932 ws,
2933 loop_ws,
2934 h_final,
2935 last_logdet,
2936 prev_deviance,
2937 max_iter,
2938 converged,
2939 lm_ridge,
2940 objective_ridge,
2941 extra,
2942 LoopDiagnostics {
2943 last_deviance_change: last_dev_delta,
2944 last_step_halving: last_halving,
2945 last_step_size,
2946 min_deviance: min_dev,
2947 step_search_exhausted,
2948 },
2949 )
2950 }
2951
2952 struct LoopDiagnostics {
2965 last_deviance_change: f64,
2966 last_step_halving: usize,
2967 last_step_size: f64,
2968 min_deviance: f64,
2969 step_search_exhausted: bool,
2970 }
2971
2972 fn build_loop_outcome(
2987 ws: &mut SigmaPirlsGpuWorkspace,
2988 loop_ws: &mut PirlsLoopWorkspace,
2989 penalized_hessian: Array2<f64>,
2990 logdet: f64,
2991 deviance: f64,
2992 iterations: usize,
2993 converged: bool,
2994 step_lm_lambda: f64,
2995 objective_ridge: f64,
2996 extra: Option<&PirlsLoopExtra<'_>>,
2997 diagnostics: LoopDiagnostics,
2998 ) -> Result<PirlsLoopOutcome, PirlsGpuLoopError> {
2999 let beta = download_vec(&ws.stream, &loop_ws.beta_dev)?;
3000 let final_eta = download_vec(&ws.stream, &loop_ws.eta_dev)?;
3001 let final_mu = download_vec(&ws.stream, &loop_ws.row_final.mu)?;
3002 let final_grad_eta = download_vec(&ws.stream, &loop_ws.row_final.grad_eta)?;
3003 let final_w_hessian = download_vec(&ws.stream, &loop_ws.row_final.w_hessian)?;
3004 let final_w_solver = download_vec(&ws.stream, &loop_ws.row_final.w_solver)?;
3005
3006 let eta_finite = final_eta.iter().all(|v| v.is_finite());
3011 let mu_finite = final_mu.iter().all(|v| v.is_finite());
3012 let beta_finite = beta.iter().all(|v| v.is_finite());
3013 let stability_ok = eta_finite && mu_finite && beta_finite;
3014 let status = if !stability_ok {
3015 crate::pirls::PirlsStatus::Unstable
3016 } else if converged {
3017 crate::pirls::PirlsStatus::Converged
3018 } else if diagnostics.step_search_exhausted {
3019 crate::pirls::PirlsStatus::LmStepSearchExhausted
3027 } else {
3028 crate::pirls::PirlsStatus::MaxIterationsReached
3029 };
3030
3031 let default_ridge = gam_problem::RidgePassport::scaled_identity(
3034 objective_ridge,
3035 gam_linalg::RidgePolicy::exact_full_objective(),
3036 )
3037 .map_err(gam_problem::EstimationError::from)?;
3038
3039 let max_abs_eta = final_eta.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
3040
3041 match extra {
3042 Some(ext) => {
3043 let (score_c, score_d, solve_dmu_deta, solve_d2mu_deta2, solve_d3mu_deta3) =
3046 crate::pirls::computeworkingweight_derivatives_from_eta(
3047 ext.likelihood,
3048 ext.inverse_link,
3049 &final_eta,
3050 ext.priorweights,
3051 )
3052 .map_err(PirlsGpuLoopError::Geometry)?;
3053
3054 let (finalweights, solve_c_array, solve_d_array) = match ext.exported_curvature {
3055 crate::pirls::HessianCurvatureKind::Observed => {
3056 crate::pirls::compute_observed_hessian_curvature_arrays(
3057 ext.likelihood,
3058 ext.inverse_link,
3059 &final_eta,
3060 ext.y,
3061 &final_w_solver,
3062 ext.priorweights,
3063 )
3064 .map_err(PirlsGpuLoopError::Geometry)?
3065 }
3066 crate::pirls::HessianCurvatureKind::Fisher => {
3067 (final_w_solver.clone(), score_c.clone(), score_d.clone())
3068 }
3069 };
3070
3071 let beta_transformed = beta.clone();
3077
3078 let constraint_kkt = ext.linear_constraints.and_then(|lin| {
3079 if lin.a.nrows() == 0 {
3080 return None;
3081 }
3082 let grad = penalized_hessian.dot(&beta);
3088 Some(crate::active_set::compute_constraint_kkt_diagnostics(
3089 &beta, &grad, lin,
3090 ))
3091 });
3092
3093 let ridge_passport = ext.ridge_passport.unwrap_or(default_ridge);
3094 let firth = ext
3095 .firth
3096 .clone()
3097 .unwrap_or(crate::pirls::FirthDiagnostics::Inactive);
3098 let edf = ext.edf.unwrap_or(f64::NAN);
3099 let derivatives_unsupported = false;
3105
3106 Ok(PirlsLoopOutcome {
3107 beta,
3108 penalized_hessian,
3109 logdet,
3110 deviance,
3111 iterations,
3112 converged,
3113 final_eta,
3114 final_mu,
3115 final_grad_eta,
3116 final_w_hessian,
3117 final_w_solver: final_w_solver.clone(),
3118 final_offset: ext.offset.to_owned(),
3119 beta_transformed,
3120 finalweights,
3121 solveweights: final_w_solver,
3122 solve_dmu_deta,
3123 solve_d2mu_deta2,
3124 solve_d3mu_deta3,
3125 solve_c_array,
3126 solve_d_array,
3127 derivatives_unsupported,
3128 status,
3129 ridge_passport,
3130 firth,
3131 constraint_kkt,
3132 edf,
3133 last_deviance_change: diagnostics.last_deviance_change,
3134 last_step_halving: diagnostics.last_step_halving,
3135 last_step_size: diagnostics.last_step_size,
3136 final_lm_lambda: step_lm_lambda,
3137 min_deviance: diagnostics.min_deviance,
3138 max_abs_eta,
3139 })
3140 }
3141 None => {
3142 Ok(PirlsLoopOutcome {
3150 beta: beta.clone(),
3151 penalized_hessian,
3152 logdet,
3153 deviance,
3154 iterations,
3155 converged,
3156 final_eta,
3157 final_mu,
3158 final_grad_eta,
3159 final_w_hessian,
3160 final_w_solver: final_w_solver.clone(),
3161 final_offset: Array1::<f64>::zeros(0),
3162 beta_transformed: beta,
3163 finalweights: Array1::<f64>::zeros(0),
3164 solveweights: final_w_solver,
3165 solve_dmu_deta: Array1::<f64>::zeros(0),
3166 solve_d2mu_deta2: Array1::<f64>::zeros(0),
3167 solve_d3mu_deta3: Array1::<f64>::zeros(0),
3168 solve_c_array: Array1::<f64>::zeros(0),
3169 solve_d_array: Array1::<f64>::zeros(0),
3170 derivatives_unsupported: true,
3171 status,
3172 ridge_passport: default_ridge,
3173 firth: crate::pirls::FirthDiagnostics::Inactive,
3174 constraint_kkt: None,
3175 edf: f64::NAN,
3176 last_deviance_change: diagnostics.last_deviance_change,
3177 last_step_halving: diagnostics.last_step_halving,
3178 last_step_size: diagnostics.last_step_size,
3179 final_lm_lambda: step_lm_lambda,
3180 min_deviance: diagnostics.min_deviance,
3181 max_abs_eta,
3182 })
3183 }
3184 }
3185 }
3186
3187 fn gemv_no_trans(
3188 blas: &CudaBlas,
3189 n: usize,
3190 p: usize,
3191 a_dev: &CudaSlice<f64>,
3192 x_dev: &CudaSlice<f64>,
3193 y_dev: &mut CudaSlice<f64>,
3194 ) -> Result<(), String> {
3195 let n_i = to_i32(n)?;
3196 let p_i = to_i32(p)?;
3197 let cfg = GemvConfig::<f64> {
3198 trans: cublasOperation_t::CUBLAS_OP_N,
3199 m: n_i,
3200 n: p_i,
3201 alpha: 1.0,
3202 lda: n_i,
3203 incx: 1,
3204 beta: 0.0,
3205 incy: 1,
3206 };
3207 unsafe { blas.gemv(cfg, a_dev, x_dev, y_dev) }.map_err(|e| format!("dgemv no-trans: {e}"))
3209 }
3210
3211 fn axpy(
3212 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3213 func: &cudarc::driver::CudaFunction,
3214 alpha: f64,
3215 x_dev: &CudaSlice<f64>,
3216 y_dev: &mut CudaSlice<f64>,
3217 n: usize,
3218 ) -> Result<(), String> {
3219 const THREADS: u32 = 256;
3220 let n_i = to_i32(n)?;
3221 let n_u = u32::try_from(n).map_err(|_| format!("axpy n={n} > u32"))?;
3222 let grid = n_u.div_ceil(THREADS).max(1);
3223 let cfg = LaunchConfig {
3224 grid_dim: (grid, 1, 1),
3225 block_dim: (THREADS, 1, 1),
3226 shared_mem_bytes: 0,
3227 };
3228 let mut builder = stream.launch_builder(func);
3229 builder.arg(&alpha);
3230 builder.arg(x_dev);
3231 builder.arg(y_dev);
3232 builder.arg(&n_i);
3233 unsafe { builder.launch(cfg) }
3236 .map(|_event_pair| ())
3237 .map_err(|e| format!("axpy launch: {e}"))
3238 }
3239
3240 fn reduce_scalar(
3241 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3242 func: &cudarc::driver::CudaFunction,
3243 src: &CudaSlice<f64>,
3244 len: usize,
3245 scalar_dev: &mut CudaSlice<f64>,
3246 label: &'static str,
3247 ) -> Result<f64, String> {
3248 const THREADS: u32 = 1024;
3249 let len_i = to_i32(len)?;
3250 let cfg = LaunchConfig {
3251 grid_dim: (1, 1, 1),
3252 block_dim: (THREADS, 1, 1),
3253 shared_mem_bytes: 0,
3254 };
3255 let mut builder = stream.launch_builder(func);
3256 builder.arg(src);
3257 builder.arg(&len_i);
3258 builder.arg(&mut *scalar_dev);
3259 unsafe { builder.launch(cfg) }.map_err(|e| format!("{label} reduce launch: {e}"))?;
3263 let host = stream
3264 .clone_dtoh(scalar_dev)
3265 .map_err(|e| format!("download {label}: {e}"))?;
3266 Ok(host[0])
3267 }
3268
3269 fn reduce_status_first(
3272 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3273 func: &cudarc::driver::CudaFunction,
3274 src: &CudaSlice<u32>,
3275 len: usize,
3276 status_dev: &mut CudaSlice<u32>,
3277 label: &'static str,
3278 ) -> Result<Option<(usize, u32)>, String> {
3279 const THREADS: u32 = 1024;
3280 let len_i = to_i32(len)?;
3281 let cfg = LaunchConfig {
3282 grid_dim: (1, 1, 1),
3283 block_dim: (THREADS, 1, 1),
3284 shared_mem_bytes: 0,
3285 };
3286 let mut builder = stream.launch_builder(func);
3287 builder.arg(src);
3288 builder.arg(&len_i);
3289 builder.arg(&mut *status_dev);
3290 unsafe { builder.launch(cfg) }.map_err(|e| format!("{label} first reduce launch: {e}"))?;
3293 let host = stream
3294 .clone_dtoh(status_dev)
3295 .map_err(|e| format!("download {label}: {e}"))?;
3296 if host[0] == u32::MAX {
3297 Ok(None)
3298 } else {
3299 Ok(Some((host[0] as usize, host[1])))
3300 }
3301 }
3302
3303 fn reduce_ladder_status_first(
3305 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3306 func: &cudarc::driver::CudaFunction,
3307 src: &CudaSlice<u32>,
3308 n: usize,
3309 status_dev: &mut CudaSlice<u32>,
3310 ) -> Result<[Option<(usize, u32)>; crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN], String>
3311 {
3312 const THREADS: u32 = 1024;
3313 let n_i = to_i32(n)?;
3314 let cfg = LaunchConfig {
3315 grid_dim: (crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN as u32, 1, 1),
3316 block_dim: (THREADS, 1, 1),
3317 shared_mem_bytes: 0,
3318 };
3319 let mut builder = stream.launch_builder(func);
3320 builder.arg(src);
3321 builder.arg(&n_i);
3322 builder.arg(&mut *status_dev);
3323 unsafe { builder.launch(cfg) }
3326 .map_err(|e| format!("alpha-ladder status reduction launch: {e}"))?;
3327 let host = stream
3328 .clone_dtoh(status_dev)
3329 .map_err(|e| format!("download alpha-ladder status summary: {e}"))?;
3330 let mut result = [None; crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN];
3331 for k in 0..crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN {
3332 if host[k] != u32::MAX {
3333 result[k] = Some((
3334 host[k] as usize,
3335 host[crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN + k],
3336 ));
3337 }
3338 }
3339 Ok(result)
3340 }
3341
3342 fn replay_row_refusal(
3343 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3344 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3345 gamma_shape: f64,
3346 row: usize,
3347 code: u32,
3348 eta: f64,
3349 y: f64,
3350 prior_weight: f64,
3351 ) -> PirlsGpuLoopError {
3352 let input = crate::gpu_kernels::pirls_row::RowInput {
3353 eta,
3354 y,
3355 prior_weight,
3356 };
3357 match crate::gpu_kernels::pirls_row::row_reweight_cpu_at(
3358 row,
3359 family,
3360 curvature,
3361 input,
3362 gamma_shape,
3363 ) {
3364 Err(error) => PirlsGpuLoopError::Geometry(error),
3365 Ok(_) => PirlsGpuLoopError::Geometry(
3366 gam_problem::EstimationError::PirlsRowGeometryUnrepresentable {
3367 row,
3368 quantity: crate::gpu_kernels::pirls_row::status_codes::quantity(code),
3369 eta,
3370 value: f64::from(code),
3371 },
3372 ),
3373 }
3374 }
3375
3376 fn certify_device_rows(
3377 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3378 status_first_func: &cudarc::driver::CudaFunction,
3379 status: &CudaSlice<u32>,
3380 status_scratch: &mut CudaSlice<u32>,
3381 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3382 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3383 gamma_shape: f64,
3384 eta: &CudaSlice<f64>,
3385 y: &CudaSlice<f64>,
3386 prior_weight: &CudaSlice<f64>,
3387 n: usize,
3388 label: &'static str,
3389 ) -> Result<(), PirlsGpuLoopError> {
3390 let Some((_row, _code)) =
3391 reduce_status_first(stream, status_first_func, status, n, status_scratch, label)?
3392 else {
3393 return Ok(());
3394 };
3395 let eta_host = stream
3396 .clone_dtoh(eta)
3397 .map_err(|error| format!("{label} refusal eta download: {error}"))?;
3398 let y_host = stream
3399 .clone_dtoh(y)
3400 .map_err(|error| format!("{label} refusal response download: {error}"))?;
3401 let prior_host = stream
3402 .clone_dtoh(prior_weight)
3403 .map_err(|error| format!("{label} refusal prior-weight download: {error}"))?;
3404 let status_host = stream
3405 .clone_dtoh(status)
3406 .map_err(|error| format!("{label} refusal status download: {error}"))?;
3407 crate::gpu_kernels::pirls_row::replay_first_refusal(
3408 family,
3409 curvature,
3410 gamma_shape,
3411 &eta_host,
3412 &y_host,
3413 &prior_host,
3414 &status_host,
3415 )
3416 .map_err(PirlsGpuLoopError::Geometry)
3417 }
3418
3419 fn download_vec(
3420 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3421 dev: &CudaSlice<f64>,
3422 ) -> Result<Array1<f64>, String> {
3423 let host = stream
3424 .clone_dtoh(dev)
3425 .map_err(|e| format!("download vec: {e}"))?;
3426 Ok(Array1::from_vec(host))
3427 }
3428
3429 pub struct GaussianPlsResult {
3431 pub beta: Array1<f64>,
3432 pub penalized_hessian: Array2<f64>,
3433 pub logdet: f64,
3434 }
3435
3436 pub fn solve_gaussian_pls_on_stream(
3439 a_orig: ArrayView2<'_, f64>,
3440 b_orig: ArrayView1<'_, f64>,
3441 s_transformed: ArrayView2<'_, f64>,
3442 linear_shift: ArrayView1<'_, f64>,
3443 prior_mean_target: ArrayView1<'_, f64>,
3444 ridge: f64,
3445 qs: Option<ArrayView2<'_, f64>>,
3446 ) -> Result<GaussianPlsResult, String> {
3447 let p = b_orig.len();
3448 if a_orig.dim() != (p, p) {
3449 return Err(format!("A shape {:?} != ({p},{p})", a_orig.dim()));
3450 }
3451 if s_transformed.dim() != (p, p) {
3452 return Err(format!("S shape {:?} != ({p},{p})", s_transformed.dim()));
3453 }
3454 if linear_shift.len() != p {
3455 return Err(format!("linear_shift len {} != p={p}", linear_shift.len()));
3456 }
3457 if prior_mean_target.len() != p {
3458 return Err(format!(
3459 "prior_mean_target len {} != p={p}",
3460 prior_mean_target.len()
3461 ));
3462 }
3463 if let Some(qs_v) = qs {
3464 if qs_v.dim() != (p, p) {
3465 return Err(format!("qs shape {:?} != ({p},{p})", qs_v.dim()));
3466 }
3467 }
3468 let (h_rotated, rhs_base) = if let Some(qs_v) = qs {
3469 let qs_owned = qs_v.to_owned();
3470 let tmp = a_orig.dot(&qs_owned);
3471 let h = qs_owned.t().dot(&tmp);
3472 let rb = qs_owned.t().dot(&b_orig);
3473 (h, rb)
3474 } else {
3475 (a_orig.to_owned(), b_orig.to_owned())
3476 };
3477 let penalized_hessian: Array2<f64> = &h_rotated + &s_transformed;
3478 let mut regularized = penalized_hessian.clone();
3479 if ridge > 0.0 {
3480 for i in 0..p {
3481 regularized[[i, i]] += ridge;
3482 }
3483 }
3484 let mut rhs_host = rhs_base;
3485 rhs_host += &linear_shift;
3486 if ridge > 0.0 {
3487 rhs_host.scaled_add(ridge, &prior_mean_target);
3488 }
3489 let (ctx, stream) = context_and_stream()?;
3490 let solver = DnHandle::new(stream.clone())
3491 .map_err(|e| format!("cusolver init (gaussian pls): {e}"))?;
3492 let pp = p.checked_mul(p).ok_or("p*p overflow (gaussian pls)")?;
3493 let mut h_dev = stream
3494 .alloc_zeros::<f64>(pp)
3495 .map_err(|e| format!("alloc H (gaussian pls): {e}"))?;
3496 let mut rhs_dev = stream
3497 .alloc_zeros::<f64>(p)
3498 .map_err(|e| format!("alloc rhs (gaussian pls): {e}"))?;
3499 let potrf_lwork_usize = potrf_query_lwork(&solver, &stream, p)?;
3500 let potrf_lwork = i32::try_from(potrf_lwork_usize)
3501 .map_err(|_| "potrf lwork overflow (gaussian pls)".to_string())?;
3502 let mut potrf_work_dev = stream
3503 .alloc_zeros::<f64>(potrf_lwork_usize.max(1))
3504 .map_err(|e| format!("alloc potrf workspace (gaussian pls): {e}"))?;
3505 let mut potrf_info_dev = stream
3506 .alloc_zeros::<i32>(1)
3507 .map_err(|e| format!("alloc potrf info (gaussian pls): {e}"))?;
3508 let mut potrs_info_dev = stream
3509 .alloc_zeros::<i32>(1)
3510 .map_err(|e| format!("alloc potrs info (gaussian pls): {e}"))?;
3511 let reg_col = to_col_major(®ularized);
3512 stream
3513 .memcpy_htod(reg_col.as_ref(), &mut h_dev)
3514 .map_err(|e| format!("upload H (gaussian pls): {e}"))?;
3515 let rhs_slice = rhs_host
3516 .as_slice()
3517 .ok_or("rhs_host not contiguous (gaussian pls)")?;
3518 stream
3519 .memcpy_htod(rhs_slice, &mut rhs_dev)
3520 .map_err(|e| format!("upload rhs (gaussian pls): {e}"))?;
3521 potrf_in_place_reuse(
3522 &solver,
3523 &stream,
3524 p,
3525 potrf_lwork,
3526 &mut h_dev,
3527 &mut potrf_work_dev,
3528 &mut potrf_info_dev,
3529 )?;
3530 potrs_in_place_reuse(
3531 &solver,
3532 &stream,
3533 p,
3534 1,
3535 &h_dev,
3536 &mut rhs_dev,
3537 &mut potrs_info_dev,
3538 )?;
3539 let logdet = cholesky_logdet_device(&stream, &ctx, p, &h_dev)?;
3540 let beta_raw = stream
3541 .clone_dtoh(&rhs_dev)
3542 .map_err(|e| format!("download beta (gaussian pls): {e}"))?;
3543 check_deferred_potrf_info(&stream, &potrf_info_dev)?;
3544 check_deferred_potrs_info(&stream, &potrs_info_dev)?;
3545 Ok(GaussianPlsResult {
3546 beta: Array1::from_vec(beta_raw),
3547 penalized_hessian,
3548 logdet,
3549 })
3550 }
3551}
3552
3553pub fn weighted_crossprod_gpu(
3554 x: ArrayView2<'_, f64>,
3555 weights: ArrayView1<'_, f64>,
3556) -> Result<Array2<f64>, String> {
3557 #[cfg(not(target_os = "linux"))]
3558 {
3559 return cpu_fallback::weighted_crossprod_cpu(x, weights);
3560 }
3561
3562 #[cfg(target_os = "linux")]
3563 {
3564 if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
3565 .map_err(|error| error.to_string())?
3566 .is_none()
3567 {
3568 return cpu_fallback::weighted_crossprod_cpu(x, weights);
3569 }
3570 cuda::weighted_crossprod(x, weights)
3571 }
3572}
3573
3574pub fn solve_pirls_step_gpu(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
3575 #[cfg(not(target_os = "linux"))]
3576 {
3577 return cpu_fallback::solve_step_cpu(input);
3578 }
3579
3580 #[cfg(target_os = "linux")]
3581 {
3582 if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
3583 .map_err(|error| error.to_string())?
3584 .is_none()
3585 {
3586 return cpu_fallback::solve_step_cpu(input);
3587 }
3588 cuda::solve_step(input)
3589 }
3590}
3591
3592#[cfg(target_os = "linux")]
3598pub fn upload_shared_pirls_gpu(
3599 x: ndarray::ArrayView2<'_, f64>,
3600 y: ndarray::ArrayView1<'_, f64>,
3601 prior_w: ndarray::ArrayView1<'_, f64>,
3602 offset: ndarray::ArrayView1<'_, f64>,
3603) -> Result<PirlsGpuSharedData, String> {
3604 gam_gpu::device_runtime::GpuRuntime::require()
3605 .map_err(|error| format!("cannot upload shared GPU PIRLS data: {error}"))?;
3606 PirlsGpuSharedData::upload_impl(x, y, prior_w, offset)
3607}
3608
3609#[cfg(target_os = "linux")]
3613pub fn allocate_sigma_pirls_workspace(
3614 shared: &PirlsGpuSharedData,
3615) -> Result<SigmaPirlsGpuWorkspace, String> {
3616 SigmaPirlsGpuWorkspace::allocate_impl(shared)
3617}
3618
3619#[cfg(target_os = "linux")]
3624pub fn upload_qs_pirls(
3625 ws: &mut SigmaPirlsGpuWorkspace,
3626 qs: ndarray::ArrayView2<'_, f64>,
3627) -> Result<(), String> {
3628 cuda::upload_qs(ws, qs)
3629}
3630
3631#[cfg(target_os = "linux")]
3634pub fn upload_qs_identity_pirls(ws: &mut SigmaPirlsGpuWorkspace) -> Result<(), String> {
3635 cuda::upload_qs_identity(ws)
3636}
3637
3638#[cfg(target_os = "linux")]
3644pub fn solve_pirls_step_on_stream(
3645 shared: &PirlsGpuSharedData,
3646 ws: &mut SigmaPirlsGpuWorkspace,
3647 input: PirlsStepStreamInput<'_>,
3648) -> Result<PirlsGpuStep, String> {
3649 cuda::solve_step_on_stream(shared, ws, input)
3650}
3651
3652#[cfg(target_os = "linux")]
3660pub fn solve_pirls_step_on_stream_device(
3661 shared: &PirlsGpuSharedData,
3662 ws: &mut SigmaPirlsGpuWorkspace,
3663 input: PirlsStepStreamDeviceInput<'_, '_>,
3664) -> Result<PirlsGpuStep, String> {
3665 cuda::solve_step_on_stream_device(shared, ws, input)
3666}
3667
3668#[cfg(target_os = "linux")]
3678pub(crate) fn pirls_loop_on_stream(
3679 shared: &PirlsGpuSharedData,
3680 ws: &mut SigmaPirlsGpuWorkspace,
3681 loop_ws: &mut cuda::PirlsLoopWorkspace,
3682 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3683 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3684 likelihood_scale: PirlsLoopLikelihoodScale,
3685 beta0: ndarray::ArrayView1<'_, f64>,
3686 penalty_hessian: ndarray::ArrayView2<'_, f64>,
3687 linear_shift: ndarray::ArrayView1<'_, f64>,
3690 constant_shift: f64,
3692 step_lm_lambda: f64,
3693 objective_ridge: f64,
3694 max_iter: usize,
3695 tol: f64,
3696 extra: Option<&cuda::PirlsLoopExtra<'_>>,
3697) -> Result<cuda::PirlsLoopOutcome, cuda::PirlsGpuLoopError> {
3698 let gamma_shape = likelihood_scale
3699 .kernel_argument(family)
3700 .map_err(cuda::PirlsGpuLoopError::Runtime)?;
3701 cuda::pirls_loop(
3702 shared,
3703 ws,
3704 loop_ws,
3705 family,
3706 curvature,
3707 gamma_shape,
3708 beta0,
3709 penalty_hessian,
3710 linear_shift,
3711 constant_shift,
3712 step_lm_lambda,
3713 objective_ridge,
3714 max_iter,
3715 tol,
3716 extra,
3717 )
3718}
3719
3720#[cfg(target_os = "linux")]
3723pub fn allocate_pirls_loop_workspace(
3724 shared: &PirlsGpuSharedData,
3725 ws: &SigmaPirlsGpuWorkspace,
3726) -> Result<cuda::PirlsLoopWorkspace, String> {
3727 cuda::PirlsLoopWorkspace::allocate(shared, &ws.stream)
3728}
3729
3730#[cfg(target_os = "linux")]
3736pub fn solve_gaussian_pls_gpu(
3737 a_orig: ndarray::ArrayView2<'_, f64>,
3738 b_orig: ndarray::ArrayView1<'_, f64>,
3739 s_transformed: ndarray::ArrayView2<'_, f64>,
3740 linear_shift: ndarray::ArrayView1<'_, f64>,
3741 prior_mean_target: ndarray::ArrayView1<'_, f64>,
3742 ridge: f64,
3743 qs: Option<ndarray::ArrayView2<'_, f64>>,
3744) -> Result<cuda::GaussianPlsResult, String> {
3745 cuda::solve_gaussian_pls_on_stream(
3746 a_orig,
3747 b_orig,
3748 s_transformed,
3749 linear_shift,
3750 prior_mean_target,
3751 ridge,
3752 qs,
3753 )
3754}
3755
3756mod cpu_fallback {
3764 use super::{PirlsGpuInput, PirlsGpuStep};
3765 use crate::estimate::reml::assembly::xt_diag_x_dense_into;
3766 use faer::Side;
3767 use gam_linalg::faer_ndarray::FaerCholesky;
3768 use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
3769
3770 pub(super) fn weighted_crossprod_cpu(
3771 x: ArrayView2<'_, f64>,
3772 weights: ArrayView1<'_, f64>,
3773 ) -> Result<Array2<f64>, String> {
3774 validate(x, weights)?;
3775 let x_owned = x.to_owned();
3776 let w_owned = weights.to_owned();
3777 let mut scratch = Array2::<f64>::zeros(x_owned.dim());
3778 Ok(xt_diag_x_dense_into(&x_owned, &w_owned, &mut scratch))
3779 }
3780
3781 pub(super) fn solve_step_cpu(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
3782 validate(input.x, input.weights)?;
3783 let (_n, p) = input.x.dim();
3784 if input.penalty_hessian.dim() != (p, p) {
3785 return Err(format!(
3786 "penalty Hessian shape {:?} does not match p={p}",
3787 input.penalty_hessian.dim()
3788 ));
3789 }
3790 if input.gradient.len() != p {
3791 return Err(format!(
3792 "gradient length {} does not match p={p}",
3793 input.gradient.len()
3794 ));
3795 }
3796 let xtwx = weighted_crossprod_cpu(input.x, input.weights)?;
3797 let mut penalized_hessian = xtwx.clone();
3799 penalized_hessian += &input.penalty_hessian;
3800 if input.objective_ridge != 0.0 {
3801 for i in 0..p {
3802 penalized_hessian[[i, i]] += input.objective_ridge;
3803 }
3804 }
3805 let mut h_step = xtwx;
3807 h_step += &input.penalty_hessian;
3808 if input.step_lm_lambda != 0.0 {
3809 for i in 0..p {
3810 h_step[[i, i]] += input.step_lm_lambda;
3811 }
3812 }
3813 let factor = h_step
3814 .cholesky(Side::Lower)
3815 .map_err(|e| format!("CPU Cholesky failed in PIRLS fallback: {e:?}"))?;
3816 let g = Array1::from_iter(input.gradient.iter().copied());
3817 let direction = factor.solvevec(&g);
3820 let logdet = 2.0 * factor.diag().iter().map(|v| v.ln()).sum::<f64>();
3822 Ok(PirlsGpuStep {
3823 penalized_hessian,
3824 direction,
3825 logdet,
3826 })
3827 }
3828
3829 fn validate(x: ArrayView2<'_, f64>, weights: ArrayView1<'_, f64>) -> Result<(), String> {
3830 let (n, p) = x.dim();
3831 if weights.len() != n {
3832 return Err(format!(
3833 "weights length {} does not match rows {n}",
3834 weights.len()
3835 ));
3836 }
3837 if n == 0 || p == 0 {
3838 return Err("empty design cannot be solved".to_string());
3839 }
3840 Ok(())
3841 }
3842}
3843
3844pub fn cholesky_solve_gpu(
3845 hessian: ArrayView2<'_, f64>,
3846 rhs: ArrayView2<'_, f64>,
3847) -> Result<(Array2<f64>, f64), String> {
3848 gam_gpu::solver::cholesky_solve_gpu(hessian, rhs)
3849}
3850
3851pub fn cholesky_solve_only_gpu(
3855 hessian: ArrayView2<'_, f64>,
3856 rhs: ArrayView2<'_, f64>,
3857) -> Result<Array2<f64>, String> {
3858 gam_gpu::solver::cholesky_solve_only_gpu(hessian, rhs)
3859}
3860
3861pub fn cholesky_lower_gpu(hessian: ArrayView2<'_, f64>) -> Result<Array2<f64>, String> {
3862 gam_gpu::solver::cholesky_lower_gpu(hessian)
3863}
3864
3865#[cfg(all(test, target_os = "linux"))]
3866mod pirls_loop_likelihood_scale_tests {
3867 use super::PirlsLoopLikelihoodScale;
3868 use crate::gpu_kernels::pirls_row::PirlsRowFamily;
3869
3870 #[test]
3871 fn gpu_row_scale_discriminant_rejects_family_mismatch() {
3872 assert!(
3873 PirlsLoopLikelihoodScale::non_gamma()
3874 .kernel_argument(PirlsRowFamily::GammaLog)
3875 .is_err()
3876 );
3877 let gamma = PirlsLoopLikelihoodScale::gamma_shape(2.0).expect("positive Gamma shape");
3878 assert!(gamma.kernel_argument(PirlsRowFamily::PoissonLog).is_err());
3879 assert_eq!(
3880 gamma
3881 .kernel_argument(PirlsRowFamily::GammaLog)
3882 .expect("matching Gamma contract"),
3883 2.0
3884 );
3885 }
3886
3887 #[test]
3888 fn non_gamma_kernel_scalar_is_poisoned_not_unit_scaled() {
3889 let abi_value = PirlsLoopLikelihoodScale::non_gamma()
3890 .kernel_argument(PirlsRowFamily::PoissonLog)
3891 .expect("matching non-Gamma contract");
3892 assert!(abi_value.is_nan());
3893 }
3894}
3895
3896#[cfg(all(test, target_os = "linux"))]
3902mod stream_device_parity_tests {
3903 use super::*;
3904 use ndarray::arr2;
3905
3906 fn device_available() -> bool {
3907 gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto)
3908 .unwrap_or_else(|error| panic!("GPU probe fault in PIRLS device test: {error}"))
3909 .is_some()
3910 }
3911
3912 #[test]
3913 fn device_input_step_matches_host_input_step_on_v100() {
3914 if !device_available() {
3915 eprintln!("[stream_device_parity] no CUDA runtime — skipping");
3916 return;
3917 }
3918 let x = arr2(&[
3919 [1.0, 0.5, 0.1],
3920 [0.2, -0.3, 1.4],
3921 [0.7, 1.1, -0.2],
3922 [-0.4, 0.9, 0.6],
3923 [0.3, -0.8, 0.5],
3924 ]);
3925 let weights = ndarray::arr1(&[1.0, 0.8, 1.2, 0.9, 1.05]);
3926 let g_eta = ndarray::arr1(&[0.10_f64, -0.20, 0.05, 0.30, -0.15]);
3930 let gradient: ndarray::Array1<f64> = x.t().dot(&g_eta);
3931 let penalty = arr2(&[[0.4, 0.0, 0.0], [0.0, 0.9, 0.0], [0.0, 0.0, 1.2]]);
3932 let lm_ridge = 0.1;
3933
3934 let n = x.nrows();
3935 let y_dummy = ndarray::Array1::<f64>::zeros(n);
3936 let prior_w_dummy = ndarray::Array1::<f64>::ones(n);
3937 let offset_dummy = ndarray::Array1::<f64>::zeros(n);
3938 let shared = upload_shared_pirls_gpu(
3939 x.view(),
3940 y_dummy.view(),
3941 prior_w_dummy.view(),
3942 offset_dummy.view(),
3943 )
3944 .expect("upload shared design");
3945 let mut ws_host = allocate_sigma_pirls_workspace(&shared).expect("alloc host-input ws");
3946 let mut ws_dev = allocate_sigma_pirls_workspace(&shared).expect("alloc device-input ws");
3947
3948 let host_step = solve_pirls_step_on_stream(
3949 &shared,
3950 &mut ws_host,
3951 PirlsStepStreamInput {
3952 weights: weights.view(),
3953 penalty_hessian: penalty.view(),
3954 gradient: gradient.view(),
3955 step_lm_lambda: lm_ridge,
3956 objective_ridge: 0.0,
3957 },
3958 )
3959 .expect("host-input step");
3960
3961 let mut w_dev = ws_dev.stream.alloc_zeros::<f64>(n).expect("alloc w_dev");
3962 let mut g_dev = ws_dev.stream.alloc_zeros::<f64>(n).expect("alloc g_dev");
3963 ws_dev
3964 .stream
3965 .memcpy_htod(weights.as_slice().unwrap(), &mut w_dev)
3966 .expect("upload w_dev");
3967 ws_dev
3968 .stream
3969 .memcpy_htod(g_eta.as_slice().unwrap(), &mut g_dev)
3970 .expect("upload g_dev");
3971
3972 let beta_dev_test = ws_dev
3973 .stream
3974 .alloc_zeros::<f64>(x.ncols())
3975 .expect("alloc beta_dev_test");
3976 let linear_shift_test = ndarray::Array1::<f64>::zeros(x.ncols());
3977 let dev_step = solve_pirls_step_on_stream_device(
3978 &shared,
3979 &mut ws_dev,
3980 PirlsStepStreamDeviceInput {
3981 w_solver_dev: &w_dev,
3982 grad_eta_dev: &g_dev,
3983 penalty_hessian: penalty.view(),
3984 step_lm_lambda: lm_ridge,
3985 objective_ridge: 0.0,
3986 beta_dev: &beta_dev_test,
3987 linear_shift: linear_shift_test.view(),
3988 },
3989 )
3990 .expect("device-input step");
3991
3992 for i in 0..3 {
3995 for j in 0..3 {
3996 let diff = (host_step.penalized_hessian[[i, j]]
3997 - dev_step.penalized_hessian[[i, j]])
3998 .abs();
3999 assert!(diff <= 1e-10, "H[{i},{j}] mismatch: {diff}");
4000 }
4001 }
4002 assert!(
4003 (host_step.logdet - dev_step.logdet).abs() <= 1e-9,
4004 "logdet mismatch: host={} dev={}",
4005 host_step.logdet,
4006 dev_step.logdet
4007 );
4008 for i in 0..3 {
4011 let diff = (host_step.direction[i] - dev_step.direction[i]).abs();
4012 assert!(diff <= 1e-9, "direction[{i}] mismatch: {diff}");
4013 }
4014 }
4015
4016 #[test]
4025 fn hill_climb_loop_beats_cpu_10x_on_large_scale_logit() {
4026 use crate::gpu_kernels::pirls_row::{
4027 CurvatureMode, PirlsRowFamily, RowInput, row_reweight_cpu,
4028 };
4029 use std::time::Instant;
4030 if !device_available() {
4031 eprintln!("[hill_climb] no CUDA runtime — skipping");
4032 return;
4033 }
4034 let n = 80_000_usize;
4035 let p = 44_usize;
4036 let beta_true: ndarray::Array1<f64> = ndarray::Array1::from_iter(
4038 (0..p).map(|j| 0.05 * ((j as f64) - 0.5 * p as f64) / p as f64),
4039 );
4040 let mut x = ndarray::Array2::<f64>::zeros((n, p));
4041 for i in 0..n {
4042 for j in 0..p {
4043 x[[i, j]] = ((i as f64 + j as f64 * 17.0) * 0.001).sin();
4044 }
4045 }
4046 let eta: ndarray::Array1<f64> = x.dot(&beta_true);
4047 let y: ndarray::Array1<f64> = eta
4048 .iter()
4049 .enumerate()
4050 .map(|(i, &e)| {
4051 let mu = 0.5 * (1.0 + (0.5 * e).tanh());
4052 if (i as f64 * 1.31).fract() < mu {
4053 1.0
4054 } else {
4055 0.0
4056 }
4057 })
4058 .collect();
4059 let prior_w = ndarray::Array1::<f64>::ones(n);
4060 let penalty = ndarray::Array2::<f64>::eye(p) * 1e-3;
4061 let beta0 = ndarray::Array1::<f64>::zeros(p);
4062
4063 let offset_bench = ndarray::Array1::<f64>::zeros(n);
4065 let shared =
4066 upload_shared_pirls_gpu(x.view(), y.view(), prior_w.view(), offset_bench.view())
4067 .expect("upload shared design");
4068 let mut ws = allocate_sigma_pirls_workspace(&shared).expect("alloc ws");
4069 let mut loop_ws = allocate_pirls_loop_workspace(&shared, &ws).expect("alloc loop_ws");
4070 let t0 = Instant::now();
4071 let linear_shift_zero = ndarray::Array1::<f64>::zeros(p);
4075 drop(
4076 pirls_loop_on_stream(
4077 &shared,
4078 &mut ws,
4079 &mut loop_ws,
4080 PirlsRowFamily::BernoulliLogit,
4081 CurvatureMode::Fisher,
4082 PirlsLoopLikelihoodScale::non_gamma(),
4083 beta0.view(),
4084 penalty.view(),
4085 linear_shift_zero.view(),
4086 0.0,
4087 0.0,
4088 0.0,
4089 30,
4090 1e-6,
4091 None,
4092 )
4093 .expect("pirls loop"),
4094 );
4095 let gpu_secs = t0.elapsed().as_secs_f64();
4096
4097 let t1 = Instant::now();
4100 let mut beta = ndarray::Array1::<f64>::zeros(p);
4101 for _ in 0..30 {
4102 let eta: ndarray::Array1<f64> = x.dot(&beta);
4103 let mut w = ndarray::Array1::<f64>::zeros(n);
4104 let mut g = ndarray::Array1::<f64>::zeros(n);
4105 for i in 0..n {
4106 let out = row_reweight_cpu(
4107 PirlsRowFamily::BernoulliLogit,
4108 CurvatureMode::Fisher,
4109 RowInput {
4110 eta: eta[i],
4111 y: y[i],
4112 prior_weight: prior_w[i],
4113 },
4114 1.0,
4115 )
4116 .expect("CPU PIRLS benchmark row must be representable");
4117 w[i] = out.w_solver;
4118 g[i] = out.grad_eta;
4119 }
4120 let mut wx_full = x.clone();
4121 for j in 0..p {
4122 for i in 0..n {
4123 wx_full[[i, j]] *= w[i];
4124 }
4125 }
4126 let h = x.t().dot(&wx_full) + &penalty;
4127 let rhs = x.t().dot(&g) - penalty.dot(&beta);
4134 use gam_linalg::faer_ndarray::FaerCholesky;
4135 let chol = h
4136 .cholesky(faer::Side::Lower)
4137 .expect("CPU PIRLS reference Cholesky");
4138 let d = chol.solvevec(&rhs);
4139 for i in 0..p {
4140 beta[i] += d[i];
4141 }
4142 }
4143 let cpu_secs = t1.elapsed().as_secs_f64();
4144
4145 let speedup = cpu_secs / gpu_secs;
4146 eprintln!(
4147 "[hill_climb] n={n} p={p} BernoulliLogit/Fisher: gpu={:.3}s cpu={:.3}s speedup={:.2}×",
4148 gpu_secs, cpu_secs, speedup
4149 );
4150 assert!(
4156 speedup >= 2.0,
4157 "GPU PIRLS loop dispatch-worthiness: got speedup={speedup:.2}× \
4158 (gpu={gpu_secs:.3}s cpu={cpu_secs:.3}s) — the resident loop must \
4159 clearly beat the same-box CPU"
4160 );
4161 }
4162
4163 #[test]
4168 fn pirls_loop_converges_to_ols_solution_on_gaussian_identity() {
4169 if !device_available() {
4170 eprintln!("[stage_3_3] no CUDA runtime — skipping");
4171 return;
4172 }
4173 let x = arr2(&[
4174 [1.0, 0.5, 0.1],
4175 [0.2, -0.3, 1.4],
4176 [0.7, 1.1, -0.2],
4177 [-0.4, 0.9, 0.6],
4178 [0.3, -0.8, 0.5],
4179 [1.1, 0.2, -0.4],
4180 [-0.6, 0.4, 0.3],
4181 [0.8, -1.0, 0.7],
4182 ]);
4183 let n = x.nrows();
4184 let p = x.ncols();
4185 let beta_true = ndarray::arr1(&[0.5_f64, -1.2, 0.3]);
4187 let y: ndarray::Array1<f64> = x.dot(&beta_true);
4188 let prior_w = ndarray::Array1::<f64>::ones(n);
4189 let penalty = ndarray::Array2::<f64>::eye(p) * 1e-4; let beta0 = ndarray::Array1::<f64>::zeros(p);
4191
4192 let offset_ols = ndarray::Array1::<f64>::zeros(n);
4193 let shared = upload_shared_pirls_gpu(x.view(), y.view(), prior_w.view(), offset_ols.view())
4194 .expect("upload shared design");
4195 let mut ws = allocate_sigma_pirls_workspace(&shared).expect("alloc ws");
4196 let mut loop_ws = allocate_pirls_loop_workspace(&shared, &ws).expect("alloc loop_ws");
4197
4198 let linear_shift_zero = ndarray::Array1::<f64>::zeros(p);
4202 let outcome = pirls_loop_on_stream(
4203 &shared,
4204 &mut ws,
4205 &mut loop_ws,
4206 crate::gpu_kernels::pirls_row::PirlsRowFamily::GaussianIdentity,
4207 crate::gpu_kernels::pirls_row::CurvatureMode::Fisher,
4208 PirlsLoopLikelihoodScale::non_gamma(),
4209 beta0.view(),
4210 penalty.view(),
4211 linear_shift_zero.view(),
4212 0.0,
4213 0.0,
4214 0.0,
4215 20,
4216 1e-9,
4217 None,
4218 )
4219 .expect("pirls loop");
4220
4221 let xtx = x.t().dot(&x);
4223 let xty = x.t().dot(&y);
4224 let h_ref = xtx + &penalty;
4225 use gam_linalg::faer_ndarray::FaerCholesky;
4227 let chol = h_ref
4228 .cholesky(faer::Side::Lower)
4229 .expect("OLS reference Cholesky");
4230 let beta_ref: ndarray::Array1<f64> = chol.solvevec(&xty);
4231
4232 assert!(
4237 outcome.converged || outcome.iterations <= 5,
4238 "PIRLS loop did not converge in 20 iters on Gaussian-identity (iters={})",
4239 outcome.iterations
4240 );
4241 for i in 0..p {
4242 let diff = (outcome.beta[i] - beta_ref[i]).abs();
4243 assert!(
4244 diff <= 1e-6,
4245 "β[{i}] mismatch: gpu={} ref={} diff={}",
4246 outcome.beta[i],
4247 beta_ref[i],
4248 diff
4249 );
4250 }
4251 for i in 0..p {
4254 for j in 0..p {
4255 let diff = (outcome.penalized_hessian[[i, j]] - h_ref[[i, j]]).abs();
4256 assert!(diff <= 1e-8, "H[{i},{j}] mismatch: {diff}");
4257 }
4258 }
4259 }
4260}
4261
4262#[cfg(test)]
4271mod weighted_crossprod_cpu_fallback_tests {
4272 use super::weighted_crossprod_gpu;
4273 use ndarray::{Array1, Array2};
4274
4275 #[test]
4276 fn weighted_crossprod_gpu_cpu_fallback_matches_dense_xtwx() {
4277 let x = Array2::<f64>::from_shape_fn((4, 3), |(i, j)| (i + j) as f64 + 1.0);
4280 let w = Array1::<f64>::from_vec(vec![0.5, 1.0, 1.5, 2.0]);
4281
4282 let got = weighted_crossprod_gpu(x.view(), w.view())
4283 .expect("weighted_crossprod_gpu must return Ok via CPU fallback on a CPU-only host");
4284
4285 let (n, p) = x.dim();
4287 let mut expected = Array2::<f64>::zeros((p, p));
4288 for k in 0..n {
4289 for i in 0..p {
4290 for j in 0..p {
4291 expected[[i, j]] += w[k] * x[[k, i]] * x[[k, j]];
4292 }
4293 }
4294 }
4295
4296 assert_eq!(got.dim(), (p, p));
4297 for i in 0..p {
4298 for j in 0..p {
4299 let diff = (got[[i, j]] - expected[[i, j]]).abs();
4300 assert!(
4301 diff <= 1e-10,
4302 "XtWX[{i},{j}] mismatch: got vs expected diff={diff}"
4303 );
4304 }
4305 }
4306 }
4307}