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")]
109pub struct PirlsStepStreamDeviceInput<'a, 'b> {
110 pub w_solver_dev: &'a cudarc::driver::CudaSlice<f64>,
113 pub grad_eta_dev: &'b cudarc::driver::CudaSlice<f64>,
116 pub penalty_hessian: ArrayView2<'b, f64>,
118 pub step_lm_lambda: f64,
121 pub objective_ridge: f64,
124 pub beta_dev: &'b cudarc::driver::CudaSlice<f64>,
127 pub linear_shift: ArrayView1<'b, f64>,
131}
132
133#[cfg(target_os = "linux")]
141pub struct PirlsGpuSharedData {
142 pub(crate) ctx: std::sync::Arc<cudarc::driver::CudaContext>,
143 pub(crate) n: usize,
144 pub(crate) p: usize,
145 pub(crate) x_original_dev: cudarc::driver::CudaSlice<f64>,
148 pub(crate) y_dev: cudarc::driver::CudaSlice<f64>,
150 pub(crate) prior_w_dev: cudarc::driver::CudaSlice<f64>,
152 pub(crate) offset_dev: cudarc::driver::CudaSlice<f64>,
154}
155
156#[cfg(target_os = "linux")]
170pub struct SigmaPirlsGpuWorkspace {
171 pub(crate) stream: std::sync::Arc<cudarc::driver::CudaStream>,
172 pub(crate) blas: cudarc::cublas::CudaBlas,
173 pub(crate) solver: cudarc::cusolver::DnHandle,
174 pub(crate) wx_dev: Option<cudarc::driver::CudaSlice<f64>>,
177 pub(crate) w_dev: cudarc::driver::CudaSlice<f64>,
178 pub(crate) xtwx_dev: cudarc::driver::CudaSlice<f64>,
180 pub(crate) h_dev: cudarc::driver::CudaSlice<f64>,
181 pub(crate) rhs_dev: cudarc::driver::CudaSlice<f64>,
182 pub(crate) penalty_dev: cudarc::driver::CudaSlice<f64>,
183 pub(crate) qs_dev: cudarc::driver::CudaSlice<f64>,
188 pub(crate) qs_tmp_dev: cudarc::driver::CudaSlice<f64>,
191 pub(crate) beta_orig_dev: cudarc::driver::CudaSlice<f64>,
193 pub(crate) dir_orig_dev: cudarc::driver::CudaSlice<f64>,
195 pub(crate) potrf_work_dev: cudarc::driver::CudaSlice<f64>,
198 pub(crate) potrf_lwork: i32,
201 pub(crate) potrf_info_dev: cudarc::driver::CudaSlice<i32>,
205 pub(crate) potrs_info_dev: cudarc::driver::CudaSlice<i32>,
207 pub(crate) n: usize,
208 pub(crate) p: usize,
209}
210
211#[cfg(target_os = "linux")]
212pub(crate) mod cuda {
213 use super::{
214 PirlsGpuInput, PirlsGpuSharedData, PirlsGpuStep, PirlsStepStreamDeviceInput,
215 PirlsStepStreamInput, SigmaPirlsGpuWorkspace,
216 };
217 use cudarc::cublas::sys::{
218 cublasDdgmm, cublasDgeam, cublasOperation_t, cublasSideMode_t, cublasStatus_t,
219 };
220 use cudarc::cublas::{CudaBlas, Gemm, GemmConfig, Gemv, GemvConfig};
221 use cudarc::cusolver::DnHandle;
222 use cudarc::driver::{CudaSlice, DevicePtr, DevicePtrMut, LaunchConfig, PushKernelArg};
223 use gam_gpu::device_cache::PtxModuleCache;
224 use gam_gpu::driver::{from_col_major, to_col_major};
225 use gam_gpu::solver::{
226 check_deferred_potrf_info, check_deferred_potrs_info, context_and_stream, pinned_htod,
227 potrf_in_place_reuse, potrf_query_lwork, potrs_in_place_reuse,
228 };
229 use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
230
231 #[derive(Debug)]
236 pub enum PirlsGpuLoopError {
237 Geometry(gam_problem::EstimationError),
238 Runtime(String),
239 }
240
241 impl std::fmt::Display for PirlsGpuLoopError {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match self {
244 Self::Geometry(error) => write!(f, "{error}"),
245 Self::Runtime(message) => f.write_str(message),
246 }
247 }
248 }
249
250 impl std::error::Error for PirlsGpuLoopError {}
251
252 impl From<String> for PirlsGpuLoopError {
253 fn from(message: String) -> Self {
254 Self::Runtime(message)
255 }
256 }
257
258 impl From<&str> for PirlsGpuLoopError {
259 fn from(message: &str) -> Self {
260 Self::Runtime(message.to_owned())
261 }
262 }
263
264 impl From<gam_problem::EstimationError> for PirlsGpuLoopError {
265 fn from(error: gam_problem::EstimationError) -> Self {
266 Self::Geometry(error)
267 }
268 }
269
270 const CHOL_LOGDET_PTX_SOURCE: &str = r#"
277extern "C" __global__ void chol_logdet_col_major(
278 const double* __restrict__ factor,
279 int p,
280 double* __restrict__ out
281) {
282 if (threadIdx.x != 0 || blockIdx.x != 0) return;
283 double acc = 0.0;
284 long long pp = (long long)p;
285 for (long long i = 0; i < pp; ++i) {
286 acc += log(factor[i * pp + i]);
287 }
288 out[0] = 2.0 * acc;
289}
290"#;
291
292 static CHOL_LOGDET_CACHE: PtxModuleCache = PtxModuleCache::new();
293
294 const FUSED_XTWX_P_THRESHOLD: usize = 256;
299
300 const FUSED_XTWX_PTX_SOURCE: &str = concat!(
310 "extern \"C\" __global__ void xtwx_lower(",
316 "const double* __restrict__ X,",
317 "const double* __restrict__ w,",
318 "double* __restrict__ A,",
319 "int n, int p) {",
320 "int t=blockIdx.x*blockDim.x+threadIdx.x;",
321 "int np=p*(p+1)/2; if(t>=np)return;",
322 "int jv=(int)((__dsqrt_rn((double)(8*t+1))-1.0)*0.5);",
324 "while((long long)(jv+1)*(jv+2)/2<=t)jv++;",
325 "while(jv>0&&(long long)jv*(jv+1)/2>t)jv--;",
326 "int kv=t-(int)((long long)jv*(jv+1)/2);",
327 "double acc=0.0;",
328 "const double*Xj=X+(long long)jv*n;",
329 "const double*Xk=X+(long long)kv*n;",
330 "for(int i=0;i<n;++i)acc+=w[i]*Xj[i]*Xk[i];",
331 "A[jv+(long long)kv*p]=acc;}",
333 "extern \"C\" __global__ void xtscore(",
335 "const double* __restrict__ X,",
336 "const double* __restrict__ score,",
337 "double* __restrict__ s,",
338 "int n, int p) {",
339 "int j=blockIdx.x*blockDim.x+threadIdx.x;",
340 "if(j>=p)return;",
341 "double acc=0.0;",
342 "const double*Xj=X+(long long)j*n;",
343 "for(int i=0;i<n;++i)acc+=score[i]*Xj[i];",
344 "s[j]=acc;}",
345 "extern \"C\" __global__ void symmetrize_lower(",
350 "double* __restrict__ A, int p) {",
351 "int ns=p*(p-1)/2;",
352 "int t=blockIdx.x*blockDim.x+threadIdx.x;",
353 "if(t>=ns)return;",
354 "int jv=(int)((__dsqrt_rn((double)(8*t+1))+1.0)*0.5);",
356 "while((long long)jv*(jv-1)/2>t)jv--;",
357 "while((long long)(jv+1)*jv/2<=t)jv++;",
358 "int kv=t-(int)((long long)jv*(jv-1)/2);",
359 "A[kv+(long long)jv*p]=A[jv+(long long)kv*p];}",
361 );
362
363 static FUSED_XTWX_CACHE: PtxModuleCache = PtxModuleCache::new();
364
365 impl PirlsGpuSharedData {
366 pub(crate) fn upload_impl(
370 x: ArrayView2<'_, f64>,
371 y: ArrayView1<'_, f64>,
372 prior_w: ArrayView1<'_, f64>,
373 offset: ArrayView1<'_, f64>,
374 ) -> Result<Self, String> {
375 let (n, p) = x.dim();
376 if n == 0 || p == 0 {
377 return Err("empty design cannot be uploaded".to_string());
378 }
379 if y.len() != n || prior_w.len() != n || offset.len() != n {
380 return Err(format!(
381 "y/prior_w/offset length mismatch (y={}, w={}, offset={}, n={n})",
382 y.len(),
383 prior_w.len(),
384 offset.len()
385 ));
386 }
387 let (ctx, stream) = context_and_stream()?;
388 let x_col = to_col_major(&x);
389 let x_original_dev = pinned_htod(&stream, &x_col)?;
390 let y_dev = pinned_htod(&stream, y.as_slice().ok_or("y not contiguous")?)?;
391 let prior_w_dev =
392 pinned_htod(&stream, prior_w.as_slice().ok_or("prior_w not contiguous")?)?;
393 let offset_dev =
394 pinned_htod(&stream, offset.as_slice().ok_or("offset not contiguous")?)?;
395 stream
399 .synchronize()
400 .map_err(|e| format!("cuda sync after model upload: {e}"))?;
401 Ok(Self {
402 ctx,
403 n,
404 p,
405 x_original_dev,
406 y_dev,
407 prior_w_dev,
408 offset_dev,
409 })
410 }
411 }
412
413 impl SigmaPirlsGpuWorkspace {
414 pub(crate) fn allocate_impl(shared: &PirlsGpuSharedData) -> Result<Self, String> {
420 let n = shared.n;
421 let p = shared.p;
422 let stream = shared
423 .ctx
424 .new_stream()
425 .map_err(|e| format!("cuda stream alloc: {e}"))?;
426 let blas = CudaBlas::new(stream.clone()).map_err(|e| format!("cublas init: {e}"))?;
427 let solver =
428 DnHandle::new(stream.clone()).map_err(|e| format!("cusolver init: {e}"))?;
429 let np = n.checked_mul(p).ok_or("X size overflow")?;
430 let pp = p.checked_mul(p).ok_or("H size overflow")?;
431 let wx_dev = if p >= FUSED_XTWX_P_THRESHOLD {
433 Some(
434 stream
435 .alloc_zeros::<f64>(np)
436 .map_err(|e| format!("cuda alloc WX: {e}"))?,
437 )
438 } else {
439 None
440 };
441 let w_dev = stream
442 .alloc_zeros::<f64>(n)
443 .map_err(|e| format!("cuda alloc W: {e}"))?;
444 let xtwx_dev = stream
445 .alloc_zeros::<f64>(pp)
446 .map_err(|e| format!("cuda alloc XtWX: {e}"))?;
447 let h_dev = stream
448 .alloc_zeros::<f64>(pp)
449 .map_err(|e| format!("cuda alloc H: {e}"))?;
450 let rhs_dev = stream
451 .alloc_zeros::<f64>(p)
452 .map_err(|e| format!("cuda alloc RHS: {e}"))?;
453 let penalty_dev = stream
454 .alloc_zeros::<f64>(pp)
455 .map_err(|e| format!("cuda alloc penalty: {e}"))?;
456 let mut qs_dev = stream
458 .alloc_zeros::<f64>(pp)
459 .map_err(|e| format!("cuda alloc Qs: {e}"))?;
460 {
462 let mut qs_host = vec![0.0_f64; pp];
463 for i in 0..p {
464 qs_host[i * p + i] = 1.0;
465 }
466 stream
467 .memcpy_htod(&qs_host, &mut qs_dev)
468 .map_err(|e| format!("init Qs identity: {e}"))?;
469 }
470 let qs_tmp_dev = stream
471 .alloc_zeros::<f64>(pp)
472 .map_err(|e| format!("cuda alloc Qs tmp: {e}"))?;
473 let beta_orig_dev = stream
474 .alloc_zeros::<f64>(p)
475 .map_err(|e| format!("cuda alloc beta_orig: {e}"))?;
476 let dir_orig_dev = stream
477 .alloc_zeros::<f64>(p)
478 .map_err(|e| format!("cuda alloc dir_orig: {e}"))?;
479 let potrf_lwork_usize = potrf_query_lwork(&solver, &stream, p)?;
483 let potrf_lwork = i32::try_from(potrf_lwork_usize)
484 .map_err(|_| format!("potrf lwork {potrf_lwork_usize} exceeds i32"))?;
485 let alloc_len = potrf_lwork_usize.max(1);
488 let potrf_work_dev = stream
489 .alloc_zeros::<f64>(alloc_len)
490 .map_err(|e| format!("cuda alloc potrf workspace: {e}"))?;
491 let potrf_info_dev = stream
492 .alloc_zeros::<i32>(1)
493 .map_err(|e| format!("cuda alloc potrf info: {e}"))?;
494 let potrs_info_dev = stream
495 .alloc_zeros::<i32>(1)
496 .map_err(|e| format!("cuda alloc potrs info: {e}"))?;
497 Ok(Self {
498 stream,
499 blas,
500 solver,
501 wx_dev,
502 w_dev,
503 xtwx_dev,
504 h_dev,
505 rhs_dev,
506 penalty_dev,
507 qs_dev,
508 qs_tmp_dev,
509 beta_orig_dev,
510 dir_orig_dev,
511 potrf_work_dev,
512 potrf_lwork,
513 potrf_info_dev,
514 potrs_info_dev,
515 n,
516 p,
517 })
518 }
519 }
520
521 pub(super) fn upload_qs(
525 ws: &mut SigmaPirlsGpuWorkspace,
526 qs: ArrayView2<'_, f64>,
527 ) -> Result<(), String> {
528 let p = ws.p;
529 if qs.dim() != (p, p) {
530 return Err(format!("upload_qs: Qs shape {:?} != ({p},{p})", qs.dim()));
531 }
532 let qs_col = to_col_major(&qs);
533 ws.stream
534 .memcpy_htod(qs_col.as_ref(), &mut ws.qs_dev)
535 .map_err(|e| format!("upload Qs: {e}"))
536 }
537
538 pub(super) fn upload_qs_identity(ws: &mut SigmaPirlsGpuWorkspace) -> Result<(), String> {
540 let p = ws.p;
541 let pp = p * p;
542 let mut qs_host = vec![0.0_f64; pp];
543 for i in 0..p {
544 qs_host[i * p + i] = 1.0;
545 }
546 ws.stream
547 .memcpy_htod(&qs_host, &mut ws.qs_dev)
548 .map_err(|e| format!("upload Qs identity: {e}"))
549 }
550
551 fn newton_step_refine_once(
560 solver: &cudarc::cusolver::DnHandle,
561 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
562 p: usize,
563 chol_factor_dev: &CudaSlice<f64>,
564 rhs_dev: &mut CudaSlice<f64>,
565 potrs_info_dev: &mut CudaSlice<i32>,
566 mut direction_raw: Vec<f64>,
567 g: &[f64],
568 penalized_hessian: &ndarray::Array2<f64>,
569 step_lm_delta: f64,
570 ) -> Result<Vec<f64>, String> {
571 use gam_gpu::policy::GpuDispatchPolicy;
572 if p < GpuDispatchPolicy::REFINEMENT_MIN_P {
573 return Ok(direction_raw);
574 }
575 let norm_g = g.iter().map(|v| v * v).sum::<f64>().sqrt();
576 if norm_g == 0.0 {
577 return Ok(direction_raw);
578 }
579 let hx: Vec<f64> = (0..p)
580 .map(|i| {
581 penalized_hessian
582 .row(i)
583 .iter()
584 .zip(direction_raw.iter())
585 .map(|(hij, xj)| hij * xj)
586 .sum::<f64>()
587 + step_lm_delta * direction_raw[i]
588 })
589 .collect();
590 let residual: Vec<f64> = g.iter().zip(hx.iter()).map(|(gi, hxi)| gi - hxi).collect();
591 let rel_res = residual.iter().map(|v| v * v).sum::<f64>().sqrt() / norm_g;
592 if rel_res <= GpuDispatchPolicy::REFINEMENT_TOL {
593 return Ok(direction_raw);
594 }
595 stream
596 .memcpy_htod(&residual, rhs_dev)
597 .map_err(|e| format!("upload residual: {e}"))?;
598 potrs_in_place_reuse(
599 solver,
600 stream,
601 p,
602 1,
603 chol_factor_dev,
604 rhs_dev,
605 potrs_info_dev,
606 )?;
607 let correction = stream
608 .clone_dtoh(rhs_dev)
609 .map_err(|e| format!("download correction: {e}"))?;
610 check_deferred_potrs_info(stream, potrs_info_dev)?;
611 for (xi, ei) in direction_raw.iter_mut().zip(correction.iter()) {
612 *xi += ei;
613 }
614 Ok(direction_raw)
615 }
616
617 pub(super) fn solve_step_on_stream(
628 shared: &PirlsGpuSharedData,
629 ws: &mut SigmaPirlsGpuWorkspace,
630 input: PirlsStepStreamInput<'_>,
631 ) -> Result<PirlsGpuStep, String> {
632 let n = shared.n;
633 let p = shared.p;
634 if ws.n != n || ws.p != p {
635 return Err(format!(
636 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
637 ws.n, ws.p
638 ));
639 }
640 if input.weights.len() != n {
641 return Err(format!(
642 "weights length {} does not match rows {n}",
643 input.weights.len()
644 ));
645 }
646 if input.penalty_hessian.dim() != (p, p) {
647 return Err(format!(
648 "penalty Hessian shape {:?} does not match p={p}",
649 input.penalty_hessian.dim()
650 ));
651 }
652 if input.gradient.len() != p {
653 return Err(format!(
654 "gradient length {} does not match p={p}",
655 input.gradient.len()
656 ));
657 }
658
659 let w_slice = input
661 .weights
662 .as_slice()
663 .ok_or("weights must be contiguous")?;
664 ws.stream
665 .memcpy_htod(w_slice, &mut ws.w_dev)
666 .map_err(|e| format!("upload W: {e}"))?;
667
668 let n_i = to_i32(n)?;
672 let p_i = to_i32(p)?;
673 if let Some(ref mut wx_dev) = ws.wx_dev {
674 left_scale_rows(
675 &ws.blas,
676 &ws.stream,
677 n,
678 p,
679 &shared.x_original_dev,
680 &mut ws.w_dev,
681 wx_dev,
682 )?;
683 let cfg = GemmConfig::<f64> {
684 transa: cublasOperation_t::CUBLAS_OP_T,
685 transb: cublasOperation_t::CUBLAS_OP_N,
686 m: p_i,
687 n: p_i,
688 k: n_i,
689 alpha: 1.0,
690 lda: n_i,
691 ldb: n_i,
692 beta: 0.0,
693 ldc: p_i,
694 };
695 unsafe {
698 ws.blas
699 .gemm(cfg, &shared.x_original_dev, wx_dev, &mut ws.xtwx_dev)
700 }
701 .map_err(|e| format!("cublas dgemm XtWX: {e}"))?;
702 } else {
703 launch_xtwx_lower(
704 &ws.stream,
705 &shared.ctx,
706 n,
707 p,
708 &shared.x_original_dev,
709 &ws.w_dev,
710 &mut ws.xtwx_dev,
711 )?;
712 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
713 }
714
715 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
717 let penalty_step_view = penalty_step.view();
718 let penalty_step_col = to_col_major(&penalty_step_view);
719 ws.stream
720 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
721 .map_err(|e| format!("upload penalty: {e}"))?;
722
723 {
727 let cfg_aq = GemmConfig::<f64> {
728 transa: cublasOperation_t::CUBLAS_OP_N,
729 transb: cublasOperation_t::CUBLAS_OP_N,
730 m: p_i,
731 n: p_i,
732 k: p_i,
733 alpha: 1.0,
734 lda: p_i,
735 ldb: p_i,
736 beta: 0.0,
737 ldc: p_i,
738 };
739 unsafe {
741 ws.blas
742 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
743 }
744 .map_err(|e| format!("dgemm A·Qs (host-input step): {e}"))?;
745 }
746 {
747 let cfg_qt = GemmConfig::<f64> {
748 transa: cublasOperation_t::CUBLAS_OP_T,
749 transb: cublasOperation_t::CUBLAS_OP_N,
750 m: p_i,
751 n: p_i,
752 k: p_i,
753 alpha: 1.0,
754 lda: p_i,
755 ldb: p_i,
756 beta: 0.0,
757 ldc: p_i,
758 };
759 unsafe {
761 ws.blas
762 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
763 }
764 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (host-input step): {e}"))?;
765 }
766 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
768
769 let g_slice = input
773 .gradient
774 .as_slice()
775 .ok_or("gradient must be contiguous")?;
776 ws.stream
777 .memcpy_htod(g_slice, &mut ws.rhs_dev)
778 .map_err(|e| format!("upload gradient: {e}"))?;
779
780 let xtwx_col = ws
784 .stream
785 .clone_dtoh(&ws.xtwx_dev)
786 .map_err(|e| format!("download XᵀWX (host-input step): {e}"))?;
787 let xtwx_host = from_col_major(&xtwx_col, p, p).ok_or("XᵀWX layout conversion failed")?;
788 let qs_col = ws
789 .stream
790 .clone_dtoh(&ws.qs_dev)
791 .map_err(|e| format!("download Qs (host-input step): {e}"))?;
792 let qs_host =
793 from_col_major(&qs_col, p, p).ok_or("Qs layout conversion failed (host-input step)")?;
794 let tmp_aq = xtwx_host.dot(&qs_host);
795 let h_rotated = qs_host.t().dot(&tmp_aq);
796 let penalty_export = penalty_with_ridge(input.penalty_hessian, input.objective_ridge);
797 let penalized_hessian = h_rotated + &penalty_export;
798
799 potrf_in_place_reuse(
802 &ws.solver,
803 &ws.stream,
804 p,
805 ws.potrf_lwork,
806 &mut ws.h_dev,
807 &mut ws.potrf_work_dev,
808 &mut ws.potrf_info_dev,
809 )?;
810 potrs_in_place_reuse(
811 &ws.solver,
812 &ws.stream,
813 p,
814 1,
815 &ws.h_dev,
816 &mut ws.rhs_dev,
817 &mut ws.potrs_info_dev,
818 )?;
819
820 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
824
825 let direction_raw = ws
827 .stream
828 .clone_dtoh(&ws.rhs_dev)
829 .map_err(|e| format!("download direction: {e}"))?;
830 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
834 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
835
836 let lm_ridge_delta = input.step_lm_lambda - input.objective_ridge;
840 let direction_raw = newton_step_refine_once(
841 &ws.solver,
842 &ws.stream,
843 p,
844 &ws.h_dev,
845 &mut ws.rhs_dev,
846 &mut ws.potrs_info_dev,
847 direction_raw,
848 g_slice,
849 &penalized_hessian,
850 lm_ridge_delta,
851 )?;
852
853 let direction = Array1::from_vec(direction_raw);
856
857 Ok(PirlsGpuStep {
858 penalized_hessian,
859 direction,
860 logdet,
861 })
862 }
863
864 pub(super) fn solve_step_on_stream_device_inplace(
875 shared: &PirlsGpuSharedData,
876 ws: &mut SigmaPirlsGpuWorkspace,
877 input: PirlsStepStreamDeviceInput<'_, '_>,
878 ) -> Result<f64, String> {
879 let n = shared.n;
880 let p = shared.p;
881 if ws.n != n || ws.p != p {
882 return Err(format!(
883 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
884 ws.n, ws.p
885 ));
886 }
887 if input.w_solver_dev.len() != n {
888 return Err(format!(
889 "w_solver_dev length {} does not match n={n}",
890 input.w_solver_dev.len()
891 ));
892 }
893 if input.grad_eta_dev.len() != n {
894 return Err(format!(
895 "grad_eta_dev length {} does not match n={n}",
896 input.grad_eta_dev.len()
897 ));
898 }
899 if input.penalty_hessian.dim() != (p, p) {
900 return Err(format!(
901 "penalty Hessian shape {:?} does not match p={p}",
902 input.penalty_hessian.dim()
903 ));
904 }
905
906 if input.linear_shift.len() != p {
907 return Err(format!(
908 "linear_shift length {} does not match p={p}",
909 input.linear_shift.len()
910 ));
911 }
912 if input.beta_dev.len() != p {
913 return Err(format!(
914 "beta_dev length {} does not match p={p}",
915 input.beta_dev.len()
916 ));
917 }
918 let n_i = to_i32(n)?;
919 let p_i = to_i32(p)?;
920
921 if let Some(ref mut wx_dev_ib) = ws.wx_dev {
924 left_scale_rows_borrowed(
926 &ws.blas,
927 &ws.stream,
928 n,
929 p,
930 &shared.x_original_dev,
931 input.w_solver_dev,
932 wx_dev_ib,
933 )?;
934 let cfg_xtx = GemmConfig::<f64> {
935 transa: cublasOperation_t::CUBLAS_OP_T,
936 transb: cublasOperation_t::CUBLAS_OP_N,
937 m: p_i,
938 n: p_i,
939 k: n_i,
940 alpha: 1.0,
941 lda: n_i,
942 ldb: n_i,
943 beta: 0.0,
944 ldc: p_i,
945 };
946 unsafe {
948 ws.blas
949 .gemm(cfg_xtx, &shared.x_original_dev, wx_dev_ib, &mut ws.xtwx_dev)
950 }
951 .map_err(|e| format!("dgemm XtWX inplace (large-p): {e}"))?;
952 let cfg_xts = GemvConfig::<f64> {
953 trans: cublasOperation_t::CUBLAS_OP_T,
954 m: n_i,
955 n: p_i,
956 alpha: 1.0,
957 lda: n_i,
958 incx: 1,
959 beta: 0.0,
960 incy: 1,
961 };
962 unsafe {
964 ws.blas.gemv(
965 cfg_xts,
966 &shared.x_original_dev,
967 input.grad_eta_dev,
968 &mut ws.rhs_dev,
969 )
970 }
971 .map_err(|e| format!("dgemv Xᵀ·score inplace (large-p): {e}"))?;
972 } else {
973 launch_xtwx_lower(
975 &ws.stream,
976 &shared.ctx,
977 n,
978 p,
979 &shared.x_original_dev,
980 input.w_solver_dev,
981 &mut ws.xtwx_dev,
982 )?;
983 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
984 launch_xtscore(
985 &ws.stream,
986 &shared.ctx,
987 n,
988 p,
989 &shared.x_original_dev,
990 input.grad_eta_dev,
991 &mut ws.rhs_dev,
992 )?;
993 }
994
995 {
998 let cfg_aq = GemmConfig::<f64> {
999 transa: cublasOperation_t::CUBLAS_OP_N,
1000 transb: cublasOperation_t::CUBLAS_OP_N,
1001 m: p_i,
1002 n: p_i,
1003 k: p_i,
1004 alpha: 1.0,
1005 lda: p_i,
1006 ldb: p_i,
1007 beta: 0.0,
1008 ldc: p_i,
1009 };
1010 unsafe {
1012 ws.blas
1013 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1014 }
1015 .map_err(|e| format!("dgemm A·Qs inplace: {e}"))?;
1016 }
1017 {
1019 let cfg_qt = GemmConfig::<f64> {
1020 transa: cublasOperation_t::CUBLAS_OP_T,
1021 transb: cublasOperation_t::CUBLAS_OP_N,
1022 m: p_i,
1023 n: p_i,
1024 k: p_i,
1025 alpha: 1.0,
1026 lda: p_i,
1027 ldb: p_i,
1028 beta: 0.0,
1029 ldc: p_i,
1030 };
1031 unsafe {
1033 ws.blas
1034 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1035 }
1036 .map_err(|e| format!("dgemm Qsᵀ·A·Qs inplace: {e}"))?;
1037 }
1038 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
1040 let penalty_step_col = to_col_major(&penalty_step);
1041 ws.stream
1042 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
1043 .map_err(|e| format!("upload penalty inplace: {e}"))?;
1044 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1045
1046 {
1050 let cfg_qts = GemvConfig::<f64> {
1051 trans: cublasOperation_t::CUBLAS_OP_T,
1052 m: p_i,
1053 n: p_i,
1054 alpha: 1.0,
1055 lda: p_i,
1056 incx: 1,
1057 beta: 0.0,
1058 incy: 1,
1059 };
1060 unsafe {
1062 ws.blas
1063 .gemv(cfg_qts, &ws.qs_dev, &ws.rhs_dev, &mut ws.beta_orig_dev)
1064 }
1065 .map_err(|e| format!("dgemv Qsᵀ·score inplace: {e}"))?;
1066 ws.stream
1067 .memcpy_dtod(&ws.beta_orig_dev, &mut ws.rhs_dev)
1068 .map_err(|e| format!("d2d Qsᵀ·score→rhs inplace: {e}"))?;
1069 }
1070 ws.stream
1076 .memcpy_htod(
1077 input
1078 .linear_shift
1079 .as_slice()
1080 .ok_or("linear_shift must be contiguous")?,
1081 &mut ws.dir_orig_dev,
1082 )
1083 .map_err(|e| format!("upload linear shift inplace: {e}"))?;
1084 let loop_module = PIRLS_LOOP_CACHE
1085 .get_or_compile(&shared.ctx, "pirls_loop", PIRLS_LOOP_PTX_SOURCE)
1086 .map_err(|e| format!("load rhs-correction module: {e}"))?;
1087 let correction_func = loop_module
1088 .load_function("correct_newton_rhs")
1089 .map_err(|e| format!("load correct_newton_rhs: {e}"))?;
1090 let cfg = LaunchConfig {
1091 grid_dim: ((p as u32).div_ceil(256).max(1), 1, 1),
1092 block_dim: (256, 1, 1),
1093 shared_mem_bytes: 0,
1094 };
1095 let mut builder = ws.stream.launch_builder(&correction_func);
1096 builder.arg(&mut ws.rhs_dev);
1097 builder.arg(&ws.penalty_dev);
1098 builder.arg(input.beta_dev);
1099 builder.arg(&ws.dir_orig_dev);
1100 builder.arg(&input.step_lm_lambda);
1101 builder.arg(&p_i);
1102 builder.arg(&mut ws.beta_orig_dev);
1103 unsafe { builder.launch(cfg) }
1107 .map_err(|e| format!("correct Newton rhs on device: {e}"))?;
1108
1109 potrf_in_place_reuse(
1111 &ws.solver,
1112 &ws.stream,
1113 p,
1114 ws.potrf_lwork,
1115 &mut ws.h_dev,
1116 &mut ws.potrf_work_dev,
1117 &mut ws.potrf_info_dev,
1118 )?;
1119 potrs_in_place_reuse(
1120 &ws.solver,
1121 &ws.stream,
1122 p,
1123 1,
1124 &ws.h_dev,
1125 &mut ws.rhs_dev,
1126 &mut ws.potrs_info_dev,
1127 )?;
1128 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
1129 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
1130 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
1131
1132 Ok(logdet)
1135 }
1136
1137 pub(super) fn rebuild_h_final(
1145 shared: &PirlsGpuSharedData,
1146 ws: &mut SigmaPirlsGpuWorkspace,
1147 w_hessian_dev: &CudaSlice<f64>,
1148 penalty_hessian: ArrayView2<'_, f64>,
1149 objective_ridge: f64,
1150 ) -> Result<Array2<f64>, String> {
1151 let n = shared.n;
1152 let p = shared.p;
1153
1154 if let Some(ref mut wx_dev_rh) = ws.wx_dev {
1156 left_scale_rows_borrowed(
1158 &ws.blas,
1159 &ws.stream,
1160 n,
1161 p,
1162 &shared.x_original_dev,
1163 w_hessian_dev,
1164 wx_dev_rh,
1165 )?;
1166 let n_i = to_i32(n)?;
1167 let p_i = to_i32(p)?;
1168 let gemm_cfg = GemmConfig::<f64> {
1169 transa: cublasOperation_t::CUBLAS_OP_T,
1170 transb: cublasOperation_t::CUBLAS_OP_N,
1171 m: p_i,
1172 n: p_i,
1173 k: n_i,
1174 alpha: 1.0,
1175 lda: n_i,
1176 ldb: n_i,
1177 beta: 0.0,
1178 ldc: p_i,
1179 };
1180 unsafe {
1183 ws.blas.gemm(
1184 gemm_cfg,
1185 &shared.x_original_dev,
1186 wx_dev_rh,
1187 &mut ws.xtwx_dev,
1188 )
1189 }
1190 .map_err(|e| format!("cublas dgemm XtWX (final H rebuild): {e}"))?;
1191 } else {
1192 launch_xtwx_lower(
1194 &ws.stream,
1195 &shared.ctx,
1196 n,
1197 p,
1198 &shared.x_original_dev,
1199 w_hessian_dev,
1200 &mut ws.xtwx_dev,
1201 )?;
1202 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1203 }
1204
1205 let p_i = to_i32(p)?;
1207 {
1209 let cfg_aq = GemmConfig::<f64> {
1210 transa: cublasOperation_t::CUBLAS_OP_N,
1211 transb: cublasOperation_t::CUBLAS_OP_N,
1212 m: p_i,
1213 n: p_i,
1214 k: p_i,
1215 alpha: 1.0,
1216 lda: p_i,
1217 ldb: p_i,
1218 beta: 0.0,
1219 ldc: p_i,
1220 };
1221 unsafe {
1223 ws.blas
1224 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1225 }
1226 .map_err(|e| format!("dgemm A·Qs (final H rebuild): {e}"))?;
1227 }
1228 {
1230 let cfg_qt = GemmConfig::<f64> {
1231 transa: cublasOperation_t::CUBLAS_OP_T,
1232 transb: cublasOperation_t::CUBLAS_OP_N,
1233 m: p_i,
1234 n: p_i,
1235 k: p_i,
1236 alpha: 1.0,
1237 lda: p_i,
1238 ldb: p_i,
1239 beta: 0.0,
1240 ldc: p_i,
1241 };
1242 unsafe {
1244 ws.blas
1245 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1246 }
1247 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (final H rebuild): {e}"))?;
1248 }
1249 let penalty = penalty_with_ridge(penalty_hessian, objective_ridge);
1250 let penalty_col = to_col_major(&penalty);
1251 ws.stream
1252 .memcpy_htod(penalty_col.as_ref(), &mut ws.penalty_dev)
1253 .map_err(|e| format!("upload penalty (final H rebuild): {e}"))?;
1254 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1255
1256 let h_col = ws
1258 .stream
1259 .clone_dtoh(&ws.h_dev)
1260 .map_err(|e| format!("download H_final: {e}"))?;
1261 from_col_major(&h_col, p, p).ok_or_else(|| "H_final layout conversion failed".to_string())
1262 }
1263
1264 pub(super) fn weighted_crossprod(
1265 x: ArrayView2<'_, f64>,
1266 weights: ArrayView1<'_, f64>,
1267 ) -> Result<Array2<f64>, String> {
1268 let (_, stream) = context_and_stream()?;
1269 let (n, p) = validate_design(x, weights)?;
1270 let blas = CudaBlas::new(stream.clone()).map_err(|e| format!("cublas init: {e}"))?;
1271 let x_col = to_col_major(&x);
1272 let x_dev = pinned_htod(&stream, &x_col)?;
1273 let mut w_dev = pinned_htod(
1274 &stream,
1275 weights.as_slice().ok_or("weights must be contiguous")?,
1276 )?;
1277 let mut wx_dev = stream
1278 .alloc_zeros::<f64>(n.checked_mul(p).ok_or("X size overflow")?)
1279 .map_err(|e| format!("cuda alloc WX: {e}"))?;
1280 left_scale_rows(&blas, &stream, n, p, &x_dev, &mut w_dev, &mut wx_dev)?;
1281 let mut h_dev = stream
1282 .alloc_zeros::<f64>(p.checked_mul(p).ok_or("H size overflow")?)
1283 .map_err(|e| format!("cuda alloc H: {e}"))?;
1284 let n_i = to_i32(n)?;
1285 let p_i = to_i32(p)?;
1286 let cfg = GemmConfig::<f64> {
1287 transa: cublasOperation_t::CUBLAS_OP_T,
1288 transb: cublasOperation_t::CUBLAS_OP_N,
1289 m: p_i,
1290 n: p_i,
1291 k: n_i,
1292 alpha: 1.0,
1293 lda: n_i,
1294 ldb: n_i,
1295 beta: 0.0,
1296 ldc: p_i,
1297 };
1298 unsafe { blas.gemm(cfg, &x_dev, &wx_dev, &mut h_dev) }
1301 .map_err(|e| format!("cublas dgemm XtWX: {e}"))?;
1302 let h_col = stream
1303 .clone_dtoh(&h_dev)
1304 .map_err(|e| format!("download H: {e}"))?;
1305 from_col_major(&h_col, p, p).ok_or_else(|| "H layout conversion failed".to_string())
1306 }
1307
1308 pub(super) fn solve_step(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
1309 let (_, p) = validate_design(input.x, input.weights)?;
1315 if input.penalty_hessian.dim() != (p, p) {
1316 return Err(format!(
1317 "penalty Hessian shape {:?} does not match p={p}",
1318 input.penalty_hessian.dim()
1319 ));
1320 }
1321 if input.gradient.len() != p {
1322 return Err(format!(
1323 "gradient length {} does not match p={p}",
1324 input.gradient.len()
1325 ));
1326 }
1327 let n_rows = input.x.nrows();
1333 let zero_n = ndarray::Array1::<f64>::zeros(n_rows);
1334 let shared =
1335 PirlsGpuSharedData::upload_impl(input.x, zero_n.view(), zero_n.view(), zero_n.view())?;
1336 let mut ws = SigmaPirlsGpuWorkspace::allocate_impl(&shared)?;
1337 solve_step_on_stream(
1338 &shared,
1339 &mut ws,
1340 PirlsStepStreamInput {
1341 weights: input.weights,
1342 penalty_hessian: input.penalty_hessian,
1343 gradient: input.gradient,
1344 step_lm_lambda: input.step_lm_lambda,
1345 objective_ridge: input.objective_ridge,
1346 },
1347 )
1348 }
1349
1350 fn validate_design(
1351 x: ArrayView2<'_, f64>,
1352 weights: ArrayView1<'_, f64>,
1353 ) -> Result<(usize, usize), String> {
1354 let (n, p) = x.dim();
1355 if weights.len() != n {
1356 return Err(format!(
1357 "weights length {} does not match rows {n}",
1358 weights.len()
1359 ));
1360 }
1361 if n == 0 || p == 0 {
1362 return Err("empty design cannot be solved on CUDA".to_string());
1363 }
1364 Ok((n, p))
1365 }
1366
1367 fn left_scale_rows(
1368 blas: &CudaBlas,
1369 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1370 n: usize,
1371 p: usize,
1372 x_dev: &CudaSlice<f64>,
1373 w_dev: &mut CudaSlice<f64>,
1374 wx_dev: &mut CudaSlice<f64>,
1375 ) -> Result<(), String> {
1376 let n_i = to_i32(n)?;
1377 let p_i = to_i32(p)?;
1378 let handle = *blas.handle();
1379 let (x_ptr, _x_record) = x_dev.device_ptr(stream);
1380 let (w_ptr, _w_record) = w_dev.device_ptr(stream);
1381 let (wx_ptr, _wx_record) = wx_dev.device_ptr_mut(stream);
1382 let status = unsafe {
1385 cublasDdgmm(
1386 handle,
1387 cublasSideMode_t::CUBLAS_SIDE_LEFT,
1388 n_i,
1389 p_i,
1390 x_ptr as *const f64,
1391 n_i,
1392 w_ptr as *const f64,
1393 1,
1394 wx_ptr as *mut f64,
1395 n_i,
1396 )
1397 };
1398 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1399 Ok(())
1400 } else {
1401 Err(format!("cublasDdgmm failed with {status:?}"))
1402 }
1403 }
1404
1405 fn left_scale_rows_borrowed(
1410 blas: &CudaBlas,
1411 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1412 n: usize,
1413 p: usize,
1414 x_dev: &CudaSlice<f64>,
1415 w_dev: &CudaSlice<f64>,
1416 wx_dev: &mut CudaSlice<f64>,
1417 ) -> Result<(), String> {
1418 let n_i = to_i32(n)?;
1419 let p_i = to_i32(p)?;
1420 let handle = *blas.handle();
1421 let (x_ptr, _x_record) = x_dev.device_ptr(stream);
1422 let (w_ptr, _w_record) = w_dev.device_ptr(stream);
1423 let (wx_ptr, _wx_record) = wx_dev.device_ptr_mut(stream);
1424 let status = unsafe {
1429 cublasDdgmm(
1430 handle,
1431 cublasSideMode_t::CUBLAS_SIDE_LEFT,
1432 n_i,
1433 p_i,
1434 x_ptr as *const f64,
1435 n_i,
1436 w_ptr as *const f64,
1437 1,
1438 wx_ptr as *mut f64,
1439 n_i,
1440 )
1441 };
1442 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1443 Ok(())
1444 } else {
1445 Err(format!("cublasDdgmm (borrowed) failed with {status:?}"))
1446 }
1447 }
1448
1449 fn geam_add_inplace(
1457 blas: &CudaBlas,
1458 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1459 p: usize,
1460 a: &mut CudaSlice<f64>,
1461 b: &CudaSlice<f64>,
1462 ) -> Result<(), String> {
1463 let p_i = to_i32(p)?;
1464 let alpha = 1.0_f64;
1465 let beta = 1.0_f64;
1466 let handle = *blas.handle();
1467 let (b_ptr, _b_record) = b.device_ptr(stream);
1468 let (a_ptr, _a_record) = a.device_ptr_mut(stream);
1469 let out_ptr = a_ptr;
1471 let status = unsafe {
1474 cublasDgeam(
1475 handle,
1476 cublasOperation_t::CUBLAS_OP_N,
1477 cublasOperation_t::CUBLAS_OP_N,
1478 p_i,
1479 p_i,
1480 &alpha,
1481 a_ptr as *const f64,
1482 p_i,
1483 &beta,
1484 b_ptr as *const f64,
1485 p_i,
1486 out_ptr as *mut f64,
1487 p_i,
1488 )
1489 };
1490 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1491 Ok(())
1492 } else {
1493 Err(format!("cublasDgeam failed with {status:?}"))
1494 }
1495 }
1496
1497 fn launch_xtwx_lower(
1501 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1502 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1503 n: usize,
1504 p: usize,
1505 x_dev: &CudaSlice<f64>,
1506 w_dev: &CudaSlice<f64>,
1507 a_dev: &mut CudaSlice<f64>,
1508 ) -> Result<(), String> {
1509 let module = FUSED_XTWX_CACHE
1510 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1511 .map_err(|e| format!("fused_xtwx module: {e}"))?;
1512 let func = module
1513 .load_function("xtwx_lower")
1514 .map_err(|e| format!("load xtwx_lower: {e}"))?;
1515 let n_i = to_i32(n)?;
1516 let p_i = to_i32(p)?;
1517 let num_pairs = p * (p + 1) / 2;
1518 let num_pairs_u32 = u32::try_from(num_pairs)
1519 .map_err(|_| format!("xtwx_lower: num_pairs {num_pairs} > u32"))?;
1520 const BLOCK: u32 = 256;
1521 let grid = num_pairs_u32.div_ceil(BLOCK).max(1);
1522 let cfg = cudarc::driver::LaunchConfig {
1523 grid_dim: (grid, 1, 1),
1524 block_dim: (BLOCK, 1, 1),
1525 shared_mem_bytes: 0,
1526 };
1527 let mut builder = stream.launch_builder(&func);
1528 builder.arg(x_dev);
1529 builder.arg(w_dev);
1530 builder.arg(a_dev);
1531 builder.arg(&n_i);
1532 builder.arg(&p_i);
1533 unsafe { builder.launch(cfg) }.map_err(|e| format!("xtwx_lower launch: {e}"))?;
1536 Ok(())
1537 }
1538
1539 fn launch_xtscore(
1542 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1543 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1544 n: usize,
1545 p: usize,
1546 x_dev: &CudaSlice<f64>,
1547 score_dev: &CudaSlice<f64>,
1548 s_dev: &mut CudaSlice<f64>,
1549 ) -> Result<(), String> {
1550 let module = FUSED_XTWX_CACHE
1551 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1552 .map_err(|e| format!("fused_xtwx module (xtscore): {e}"))?;
1553 let func = module
1554 .load_function("xtscore")
1555 .map_err(|e| format!("load xtscore: {e}"))?;
1556 let n_i = to_i32(n)?;
1557 let p_i = to_i32(p)?;
1558 let p_u32 = u32::try_from(p).map_err(|_| format!("xtscore: p {p} > u32"))?;
1559 const BLOCK: u32 = 256;
1560 let grid = p_u32.div_ceil(BLOCK).max(1);
1561 let cfg = cudarc::driver::LaunchConfig {
1562 grid_dim: (grid, 1, 1),
1563 block_dim: (BLOCK, 1, 1),
1564 shared_mem_bytes: 0,
1565 };
1566 let mut builder = stream.launch_builder(&func);
1567 builder.arg(x_dev);
1568 builder.arg(score_dev);
1569 builder.arg(s_dev);
1570 builder.arg(&n_i);
1571 builder.arg(&p_i);
1572 unsafe { builder.launch(cfg) }.map_err(|e| format!("xtscore launch: {e}"))?;
1575 Ok(())
1576 }
1577
1578 fn launch_symmetrize_lower(
1582 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1583 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1584 p: usize,
1585 a_dev: &mut CudaSlice<f64>,
1586 ) -> Result<(), String> {
1587 if p <= 1 {
1588 return Ok(());
1589 }
1590 let module = FUSED_XTWX_CACHE
1591 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1592 .map_err(|e| format!("fused_xtwx module (sym): {e}"))?;
1593 let func = module
1594 .load_function("symmetrize_lower")
1595 .map_err(|e| format!("load symmetrize_lower: {e}"))?;
1596 let p_i = to_i32(p)?;
1597 let num_strict = p * (p - 1) / 2;
1598 let num_strict_u32 = u32::try_from(num_strict)
1599 .map_err(|_| format!("symmetrize_lower: num_strict {num_strict} > u32"))?;
1600 const BLOCK: u32 = 256;
1601 let grid = num_strict_u32.div_ceil(BLOCK).max(1);
1602 let cfg = cudarc::driver::LaunchConfig {
1603 grid_dim: (grid, 1, 1),
1604 block_dim: (BLOCK, 1, 1),
1605 shared_mem_bytes: 0,
1606 };
1607 let mut builder = stream.launch_builder(&func);
1608 builder.arg(a_dev);
1609 builder.arg(&p_i);
1610 unsafe { builder.launch(cfg) }.map_err(|e| format!("symmetrize_lower launch: {e}"))?;
1613 Ok(())
1614 }
1615
1616 fn cholesky_logdet_device(
1621 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1622 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1623 p: usize,
1624 factor_dev: &CudaSlice<f64>,
1625 ) -> Result<f64, String> {
1626 let module = CHOL_LOGDET_CACHE
1627 .get_or_compile(ctx, "pirls_gpu_chol_logdet", CHOL_LOGDET_PTX_SOURCE)
1628 .map_err(|err| format!("chol_logdet module: {err}"))?;
1629 let func = module
1630 .load_function("chol_logdet_col_major")
1631 .map_err(|err| format!("chol_logdet load_function: {err}"))?;
1632 let mut out_dev = stream
1633 .alloc_zeros::<f64>(1)
1634 .map_err(|err| format!("alloc chol_logdet out: {err}"))?;
1635 let p_i = to_i32(p)?;
1636 let cfg = LaunchConfig {
1637 grid_dim: (1, 1, 1),
1638 block_dim: (1, 1, 1),
1639 shared_mem_bytes: 0,
1640 };
1641 let mut builder = stream.launch_builder(&func);
1642 builder.arg(factor_dev);
1643 builder.arg(&p_i);
1644 builder.arg(&mut out_dev);
1645 unsafe { builder.launch(cfg) }.map_err(|err| format!("chol_logdet launch: {err}"))?;
1650 let out_host = stream
1651 .clone_dtoh(&out_dev)
1652 .map_err(|err| format!("download chol_logdet: {err}"))?;
1653 Ok(out_host[0])
1654 }
1655
1656 fn penalty_with_ridge(penalty: ArrayView2<'_, f64>, ridge: f64) -> Array2<f64> {
1657 let mut out = penalty.to_owned();
1658 if ridge != 0.0 {
1659 for i in 0..out.nrows().min(out.ncols()) {
1660 out[[i, i]] += ridge;
1661 }
1662 }
1663 out
1664 }
1665
1666 fn to_i32(value: usize) -> Result<i32, String> {
1667 i32::try_from(value).map_err(|_| format!("CUDA dimension {value} exceeds i32"))
1668 }
1669
1670 const PIRLS_LOOP_PTX_SOURCE: &str = r#"
1677// __device__ annotation required by newer NVRTC JIT semantics (see
1678// gpu_kernels/pirls_row.rs common_device_prolog — the #2313 hardware sweep).
1679extern "C" {
1680 __device__ double fabs(double);
1681}
1682
1683extern "C" __global__ void axpy_n(
1684 double alpha,
1685 const double* __restrict__ x,
1686 double* __restrict__ y,
1687 int n
1688) {
1689 int i = blockIdx.x * blockDim.x + threadIdx.x;
1690 if (i >= n) return;
1691 y[i] += alpha * x[i];
1692}
1693
1694// Correct the projected score in place:
1695// rhs = Qs^T score - S beta + linear_shift.
1696// `penalty_step` stores S + lm*I for the factorization, so subtracting its
1697// product and adding lm*beta recovers the model penalty S exactly.
1698extern "C" __global__ void correct_newton_rhs(
1699 double* __restrict__ rhs,
1700 const double* __restrict__ penalty_step,
1701 const double* __restrict__ beta,
1702 const double* __restrict__ linear_shift,
1703 double lm,
1704 int p,
1705 double* __restrict__ penalty_beta
1706) {
1707 int i = blockIdx.x * blockDim.x + threadIdx.x;
1708 if (i >= p) return;
1709 double s_beta = 0.0;
1710 for (int j = 0; j < p; ++j) {
1711 s_beta += penalty_step[i + j * p] * beta[j];
1712 }
1713 penalty_beta[i] = s_beta;
1714 rhs[i] += -s_beta + lm * beta[i] + linear_shift[i];
1715}
1716
1717extern "C" __global__ void apply_penalty(
1718 const double* __restrict__ penalty_step,
1719 const double* __restrict__ vector,
1720 int p,
1721 double* __restrict__ output
1722) {
1723 int i = blockIdx.x * blockDim.x + threadIdx.x;
1724 if (i >= p) return;
1725 double value = 0.0;
1726 for (int j = 0; j < p; ++j) {
1727 value += penalty_step[i + j * p] * vector[j];
1728 }
1729 output[i] = value;
1730}
1731
1732// Select the first acceptable member of the seven-point line-search ladder
1733// without exporting beta, direction, objectives, or refusal summaries.
1734//
1735// output layout:
1736// [0] alpha, [1] accepted data deviance, [2] accepted penalized objective,
1737// [3] halving index, [4] ||direction||_inf,
1738// [5] first refusal row, [6] first refusal code, [7] all-refused flag.
1739extern "C" __global__ void select_alpha(
1740 const double* __restrict__ data_deviance,
1741 const unsigned int* __restrict__ refusal_summary,
1742 const double* __restrict__ beta,
1743 const double* __restrict__ direction,
1744 const double* __restrict__ penalty_beta_step,
1745 const double* __restrict__ penalty_direction_step,
1746 const double* __restrict__ linear_shift,
1747 const double* __restrict__ direction_linf,
1748 double previous_deviance,
1749 double previous_objective,
1750 double constant_shift,
1751 double lm,
1752 int p,
1753 double* __restrict__ output
1754) {
1755 if (blockIdx.x != 0 || threadIdx.x != 0) return;
1756 const double alphas[7] = {
1757 1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625
1758 };
1759
1760 double penalty_beta = constant_shift;
1761 double linear_coeff_half = 0.0;
1762 double direction_penalty = 0.0;
1763 for (int i = 0; i < p; ++i) {
1764 double s_beta = penalty_beta_step[i] - lm * beta[i];
1765 double s_direction = penalty_direction_step[i] - lm * direction[i];
1766 penalty_beta += beta[i] * s_beta - 2.0 * beta[i] * linear_shift[i];
1767 linear_coeff_half += direction[i] * (s_beta - linear_shift[i]);
1768 direction_penalty += direction[i] * s_direction;
1769 }
1770
1771 output[0] = 0.0;
1772 output[1] = previous_deviance;
1773 output[2] = previous_objective;
1774 output[3] = 0.0;
1775 output[4] = direction_linf[0];
1776 output[5] = 4294967295.0;
1777 output[6] = 0.0;
1778 output[7] = 1.0;
1779
1780 for (int k = 0; k < 7; ++k) {
1781 unsigned int row = refusal_summary[k];
1782 unsigned int code = refusal_summary[7 + k];
1783 if (k == 0 && row != 0xffffffffu) {
1784 output[5] = (double)row;
1785 output[6] = (double)code;
1786 }
1787 if (row == 0xffffffffu) {
1788 output[7] = 0.0;
1789 double alpha = alphas[k];
1790 double objective = data_deviance[k]
1791 + penalty_beta
1792 + 2.0 * alpha * linear_coeff_half
1793 + alpha * alpha * direction_penalty;
1794 if (isfinite(objective) && objective <= previous_objective) {
1795 output[0] = alpha;
1796 output[1] = data_deviance[k];
1797 output[2] = objective;
1798 output[3] = (double)k;
1799 return;
1800 }
1801 }
1802 }
1803}
1804
1805extern "C" __global__ void deviance_sum(
1806 const double* __restrict__ d,
1807 int n,
1808 double* __restrict__ out
1809) {
1810 __shared__ double sm[1024];
1811 int tid = threadIdx.x;
1812 int bdim = blockDim.x;
1813 double acc = 0.0;
1814 for (int i = tid; i < n; i += bdim) {
1815 acc += d[i];
1816 }
1817 sm[tid] = acc;
1818 __syncthreads();
1819 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
1820 if (tid < stride) sm[tid] += sm[tid + stride];
1821 __syncthreads();
1822 }
1823 if (tid == 0) out[0] = sm[0];
1824}
1825
1826extern "C" __global__ void linf_norm(
1827 const double* __restrict__ v,
1828 int p,
1829 double* __restrict__ out
1830) {
1831 __shared__ double sm[1024];
1832 int tid = threadIdx.x;
1833 int bdim = blockDim.x;
1834 double acc = 0.0;
1835 for (int i = tid; i < p; i += bdim) {
1836 double a = fabs(v[i]);
1837 if (a > acc) acc = a;
1838 }
1839 sm[tid] = acc;
1840 __syncthreads();
1841 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
1842 if (tid < stride) {
1843 double r = sm[tid + stride];
1844 if (r > sm[tid]) sm[tid] = r;
1845 }
1846 __syncthreads();
1847 }
1848 if (tid == 0) out[0] = sm[0];
1849}
1850
1851extern "C" __global__ void negate_n(
1852 double* __restrict__ v,
1853 int n
1854) {
1855 int i = blockIdx.x * blockDim.x + threadIdx.x;
1856 if (i >= n) return;
1857 v[i] = -v[i];
1858}
1859
1860// Deterministically select the smallest failing row. out[0] is UINT_MAX on
1861// success, otherwise the row index; out[1] carries that row's refusal code.
1862extern "C" __global__ void status_first(
1863 const unsigned int* __restrict__ status,
1864 int n,
1865 unsigned int* __restrict__ out
1866) {
1867 __shared__ unsigned int sm_row[1024];
1868 __shared__ unsigned int sm_code[1024];
1869 int tid = threadIdx.x;
1870 int bdim = blockDim.x;
1871 unsigned int best_row = 0xffffffffu;
1872 unsigned int best_code = 0u;
1873 for (int i = tid; i < n; i += bdim) {
1874 unsigned int code = status[i];
1875 if (code != 0u && (unsigned int)i < best_row) {
1876 best_row = (unsigned int)i;
1877 best_code = code;
1878 }
1879 }
1880 sm_row[tid] = best_row;
1881 sm_code[tid] = best_code;
1882 __syncthreads();
1883 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
1884 if (tid < stride && sm_row[tid + stride] < sm_row[tid]) {
1885 sm_row[tid] = sm_row[tid + stride];
1886 sm_code[tid] = sm_code[tid + stride];
1887 }
1888 __syncthreads();
1889 }
1890 if (tid == 0) {
1891 out[0] = sm_row[0];
1892 out[1] = sm_code[0];
1893 }
1894}
1895
1896// Same deterministic reduction for the alpha-major [7*n] ladder status
1897// matrix. One block handles each alpha; outputs are row[0..7), code[7..14).
1898extern "C" __global__ void status_first_ladder(
1899 const unsigned int* __restrict__ status,
1900 int n,
1901 unsigned int* __restrict__ out
1902) {
1903 __shared__ unsigned int sm_row[1024];
1904 __shared__ unsigned int sm_code[1024];
1905 int k = blockIdx.x;
1906 int tid = threadIdx.x;
1907 int bdim = blockDim.x;
1908 unsigned int best_row = 0xffffffffu;
1909 unsigned int best_code = 0u;
1910 const unsigned int* candidate = status + ((long long)k * n);
1911 for (int i = tid; i < n; i += bdim) {
1912 unsigned int code = candidate[i];
1913 if (code != 0u && (unsigned int)i < best_row) {
1914 best_row = (unsigned int)i;
1915 best_code = code;
1916 }
1917 }
1918 sm_row[tid] = best_row;
1919 sm_code[tid] = best_code;
1920 __syncthreads();
1921 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
1922 if (tid < stride && sm_row[tid + stride] < sm_row[tid]) {
1923 sm_row[tid] = sm_row[tid + stride];
1924 sm_code[tid] = sm_code[tid + stride];
1925 }
1926 __syncthreads();
1927 }
1928 if (tid == 0) {
1929 out[k] = sm_row[0];
1930 out[7 + k] = sm_code[0];
1931 }
1932}
1933"#;
1934
1935 static PIRLS_LOOP_CACHE: PtxModuleCache = PtxModuleCache::new();
1936
1937 pub struct PirlsLoopWorkspace {
1944 pub beta_dev: CudaSlice<f64>,
1945 pub linear_shift_dev: CudaSlice<f64>,
1947 pub eta_dev: CudaSlice<f64>,
1948 pub row_solve: crate::gpu_kernels::pirls_row::SolveRowBuffers,
1950 pub alpha_ladder: crate::gpu_kernels::pirls_row::AlphaLadderDevBuffers,
1952 pub row_final: crate::gpu_kernels::pirls_row::RowOutputDevBuffers,
1954 pub direction_dev: CudaSlice<f64>,
1955 pub penalty_direction_dev: CudaSlice<f64>,
1957 pub xd_dev: CudaSlice<f64>,
1958 pub scalar_dev: CudaSlice<f64>,
1959 pub alpha_selection_dev: CudaSlice<f64>,
1963 pub status_u32_dev: CudaSlice<u32>,
1966 pub n: usize,
1967 pub p: usize,
1968 }
1969
1970 impl PirlsLoopWorkspace {
1971 pub fn allocate(
1972 shared: &PirlsGpuSharedData,
1973 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1974 ) -> Result<Self, String> {
1975 let n = shared.n;
1976 let p = shared.p;
1977 let alloc_f64 = |label: &'static str, len: usize| {
1978 stream
1979 .alloc_zeros::<f64>(len)
1980 .map_err(|e| format!("pirls loop alloc {label}: {e}"))
1981 };
1982 Ok(Self {
1983 beta_dev: alloc_f64("beta", p)?,
1984 linear_shift_dev: alloc_f64("linear shift", p)?,
1985 eta_dev: alloc_f64("eta", n)?,
1986 row_solve: crate::gpu_kernels::pirls_row::SolveRowBuffers::allocate(stream, n)
1987 .map_err(|e| format!("pirls loop alloc row_solve: {e}"))?,
1988 alpha_ladder: crate::gpu_kernels::pirls_row::AlphaLadderDevBuffers::allocate(
1989 stream, n,
1990 )
1991 .map_err(|e| format!("pirls loop alloc alpha_ladder: {e}"))?,
1992 row_final: crate::gpu_kernels::pirls_row::RowOutputDevBuffers::allocate(stream, n)
1993 .map_err(|e| format!("pirls loop alloc row_final: {e}"))?,
1994 direction_dev: alloc_f64("direction", p)?,
1995 penalty_direction_dev: alloc_f64("penalty direction", p)?,
1996 xd_dev: alloc_f64("xd", n)?,
1997 scalar_dev: alloc_f64("scalar", 1)?,
1998 alpha_selection_dev: alloc_f64("alpha selection", 8)?,
1999 status_u32_dev: stream
2000 .alloc_zeros::<u32>(14)
2001 .map_err(|e| format!("pirls loop alloc status_u32: {e}"))?,
2002 n,
2003 p,
2004 })
2005 }
2006 }
2007
2008 pub struct PirlsLoopExtra<'a> {
2032 pub likelihood: &'a gam_problem::GlmLikelihoodSpec,
2037 pub inverse_link: &'a gam_problem::InverseLink,
2040 pub y: ndarray::ArrayView1<'a, f64>,
2043 pub priorweights: ndarray::ArrayView1<'a, f64>,
2046 pub offset: ndarray::ArrayView1<'a, f64>,
2050 pub linear_constraints: Option<&'a gam_problem::LinearInequalityConstraints>,
2057 pub exported_curvature: crate::pirls::HessianCurvatureKind,
2067 pub ridge_passport: Option<gam_problem::RidgePassport>,
2074 pub firth: Option<crate::pirls::FirthDiagnostics>,
2080 pub edf: Option<f64>,
2089 }
2090
2091 #[derive(Clone, Debug)]
2092 pub struct PirlsLoopOutcome {
2093 pub beta: Array1<f64>,
2094 pub penalized_hessian: Array2<f64>,
2095 pub logdet: f64,
2096 pub deviance: f64,
2097 pub iterations: usize,
2098 pub converged: bool,
2099 pub final_eta: Array1<f64>,
2102 pub final_mu: Array1<f64>,
2105 pub final_grad_eta: Array1<f64>,
2109 pub final_w_hessian: Array1<f64>,
2113 pub final_w_solver: Array1<f64>,
2116 pub final_offset: Array1<f64>,
2120 pub beta_transformed: Array1<f64>,
2125 pub finalweights: Array1<f64>,
2129 pub solveweights: Array1<f64>,
2132 pub solve_dmu_deta: Array1<f64>,
2136 pub solve_d2mu_deta2: Array1<f64>,
2138 pub solve_d3mu_deta3: Array1<f64>,
2140 pub solve_c_array: Array1<f64>,
2144 pub solve_d_array: Array1<f64>,
2147 pub derivatives_unsupported: bool,
2151 pub status: crate::pirls::PirlsStatus,
2157 pub ridge_passport: gam_problem::RidgePassport,
2162 pub firth: crate::pirls::FirthDiagnostics,
2165 pub constraint_kkt: Option<crate::active_set::ConstraintKktDiagnostics>,
2169 pub edf: f64,
2172 pub last_deviance_change: f64,
2176 pub last_step_halving: usize,
2182 pub last_step_size: f64,
2185 pub final_lm_lambda: f64,
2191 pub min_deviance: f64,
2198 pub max_abs_eta: f64,
2203 }
2204
2205 pub(super) fn pirls_loop(
2210 shared: &PirlsGpuSharedData,
2211 ws: &mut SigmaPirlsGpuWorkspace,
2212 loop_ws: &mut PirlsLoopWorkspace,
2213 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
2214 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
2215 gamma_shape: f64,
2218 beta0_host: ArrayView1<'_, f64>,
2219 penalty_hessian: ArrayView2<'_, f64>,
2220 linear_shift: ArrayView1<'_, f64>,
2225 constant_shift: f64,
2228 lm_ridge: f64,
2231 objective_ridge: f64,
2234 max_iter: usize,
2235 tol: f64,
2236 extra: Option<&PirlsLoopExtra<'_>>,
2237 ) -> Result<PirlsLoopOutcome, PirlsGpuLoopError> {
2238 let n = shared.n;
2239 let p = shared.p;
2240 if loop_ws.n != n || loop_ws.p != p {
2241 return Err(format!(
2242 "loop workspace ({}, {}) ≠ shared ({n}, {p})",
2243 loop_ws.n, loop_ws.p
2244 )
2245 .into());
2246 }
2247 if beta0_host.len() != p {
2248 return Err(format!("beta0 length {} ≠ p={p}", beta0_host.len()).into());
2249 }
2250
2251 if linear_shift.len() != p {
2252 return Err(format!("linear_shift length {} ≠ p={p}", linear_shift.len()).into());
2253 }
2254 if penalty_hessian.dim() != (p, p) {
2255 return Err(format!(
2256 "penalty_hessian shape {:?} ≠ (p={p}, p={p})",
2257 penalty_hessian.dim()
2258 )
2259 .into());
2260 }
2261
2262 ws.stream
2263 .memcpy_htod(
2264 beta0_host.as_slice().ok_or("beta0 not contiguous")?,
2265 &mut loop_ws.beta_dev,
2266 )
2267 .map_err(|e| format!("upload beta0: {e}"))?;
2268 ws.stream
2269 .memcpy_htod(
2270 linear_shift
2271 .as_slice()
2272 .ok_or("linear_shift not contiguous")?,
2273 &mut loop_ws.linear_shift_dev,
2274 )
2275 .map_err(|e| format!("upload linear_shift: {e}"))?;
2276
2277 let backend = crate::gpu_kernels::pirls_row::PirlsRowBackend::probe()
2278 .map_err(|e| format!("pirls_row backend: {e}"))?;
2279 let loop_module = PIRLS_LOOP_CACHE
2280 .get_or_compile(&shared.ctx, "pirls_loop", PIRLS_LOOP_PTX_SOURCE)
2281 .map_err(|e| format!("pirls loop module: {e}"))?;
2282 let axpy_func = loop_module
2283 .load_function("axpy_n")
2284 .map_err(|e| format!("load axpy_n: {e}"))?;
2285 let sum_func = loop_module
2286 .load_function("deviance_sum")
2287 .map_err(|e| format!("load deviance_sum: {e}"))?;
2288 let linf_func = loop_module
2289 .load_function("linf_norm")
2290 .map_err(|e| format!("load linf_norm: {e}"))?;
2291 let status_first_func = loop_module
2292 .load_function("status_first")
2293 .map_err(|e| format!("load status_first: {e}"))?;
2294 let status_first_ladder_func = loop_module
2295 .load_function("status_first_ladder")
2296 .map_err(|e| format!("load status_first_ladder: {e}"))?;
2297 let select_alpha_func = loop_module
2298 .load_function("select_alpha")
2299 .map_err(|e| format!("load select_alpha: {e}"))?;
2300 let apply_penalty_func = loop_module
2301 .load_function("apply_penalty")
2302 .map_err(|e| format!("load apply_penalty: {e}"))?;
2303
2304 gemv_no_trans(
2307 &ws.blas,
2308 p,
2309 p,
2310 &ws.qs_dev,
2311 &loop_ws.beta_dev,
2312 &mut ws.beta_orig_dev,
2313 )?;
2314 gemv_no_trans(
2316 &ws.blas,
2317 n,
2318 p,
2319 &shared.x_original_dev,
2320 &ws.beta_orig_dev,
2321 &mut loop_ws.eta_dev,
2322 )?;
2323 axpy(
2324 &ws.stream,
2325 &axpy_func,
2326 1.0,
2327 &shared.offset_dev,
2328 &mut loop_ws.eta_dev,
2329 n,
2330 )?;
2331 crate::gpu_kernels::pirls_row::launch_solve_row_on_stream(
2333 backend,
2334 family,
2335 curvature,
2336 gamma_shape,
2337 &ws.stream,
2338 n,
2339 &loop_ws.eta_dev,
2340 &shared.y_dev,
2341 &shared.prior_w_dev,
2342 &mut loop_ws.row_solve,
2343 )
2344 .map_err(|e| format!("solve-row init: {e}"))?;
2345 certify_device_rows(
2346 &ws.stream,
2347 &status_first_func,
2348 &loop_ws.row_solve.status,
2349 &mut loop_ws.status_u32_dev,
2350 family,
2351 curvature,
2352 gamma_shape,
2353 &loop_ws.eta_dev,
2354 &shared.y_dev,
2355 &shared.prior_w_dev,
2356 n,
2357 "solve-row init",
2358 )?;
2359
2360 let mut prev_deviance = reduce_scalar(
2361 &ws.stream,
2362 &sum_func,
2363 &loop_ws.row_solve.deviance,
2364 n,
2365 &mut loop_ws.scalar_dev,
2366 "deviance_init",
2367 )?;
2368 let mut last_logdet = 0.0_f64;
2369 let mut converged = false;
2370
2371 let s_beta0 = penalty_hessian.dot(&beta0_host);
2376 let penalty_init =
2377 beta0_host.dot(&s_beta0) - 2.0 * beta0_host.dot(&linear_shift) + constant_shift;
2378 let mut prev_objective = prev_deviance + penalty_init;
2379
2380 let mut last_dev_delta = 0.0_f64;
2388 let mut last_halving: usize = 0;
2389 let mut last_step_size = 0.0_f64;
2390 let mut min_dev = prev_deviance;
2391 let mut step_search_exhausted = false;
2392
2393 for it in 0..max_iter {
2394 last_logdet = solve_step_on_stream_device_inplace(
2395 shared,
2396 ws,
2397 PirlsStepStreamDeviceInput {
2398 w_solver_dev: &loop_ws.row_solve.w_solver,
2399 grad_eta_dev: &loop_ws.row_solve.grad_eta,
2400 penalty_hessian,
2401 step_lm_lambda: lm_ridge,
2402 objective_ridge,
2403 beta_dev: &loop_ws.beta_dev,
2404 linear_shift,
2405 },
2406 )
2407 .map_err(|e| format!("inner step it={it}: {e}"))?;
2408 ws.stream
2411 .memcpy_dtod(&ws.rhs_dev, &mut loop_ws.direction_dev)
2412 .map_err(|e| format!("direction d2d copy it={it}: {e}"))?;
2413
2414 launch_scalar_reduction(
2415 &ws.stream,
2416 &linf_func,
2417 &loop_ws.direction_dev,
2418 p,
2419 &mut loop_ws.scalar_dev,
2420 "dir_linf",
2421 )?;
2422
2423 gemv_no_trans(
2425 &ws.blas,
2426 p,
2427 p,
2428 &ws.qs_dev,
2429 &loop_ws.direction_dev,
2430 &mut ws.dir_orig_dev,
2431 )?;
2432 gemv_no_trans(
2433 &ws.blas,
2434 n,
2435 p,
2436 &shared.x_original_dev,
2437 &ws.dir_orig_dev,
2438 &mut loop_ws.xd_dev,
2439 )?;
2440
2441 loop_ws
2450 .alpha_ladder
2451 .zero(&ws.stream)
2452 .map_err(|e| format!("ladder zero it={it}: {e}"))?;
2453 crate::gpu_kernels::pirls_row::launch_alpha_ladder_on_stream(
2454 backend,
2455 family,
2456 curvature,
2457 gamma_shape,
2458 &ws.stream,
2459 n,
2460 &loop_ws.eta_dev,
2461 &loop_ws.xd_dev,
2462 &shared.y_dev,
2463 &shared.prior_w_dev,
2464 &mut loop_ws.alpha_ladder,
2465 )
2466 .map_err(|e| format!("alpha-ladder it={it}: {e}"))?;
2467 launch_ladder_status_first_reduction(
2468 &ws.stream,
2469 &status_first_ladder_func,
2470 &loop_ws.alpha_ladder.status_dev,
2471 n,
2472 &mut loop_ws.status_u32_dev,
2473 )?;
2474 let p_i = to_i32(p)?;
2475 let contraction_cfg = LaunchConfig {
2476 grid_dim: ((p as u32).div_ceil(256).max(1), 1, 1),
2477 block_dim: (256, 1, 1),
2478 shared_mem_bytes: 0,
2479 };
2480 let mut contraction_builder = ws.stream.launch_builder(&apply_penalty_func);
2481 contraction_builder.arg(&ws.penalty_dev);
2482 contraction_builder.arg(&loop_ws.direction_dev);
2483 contraction_builder.arg(&p_i);
2484 contraction_builder.arg(&mut loop_ws.penalty_direction_dev);
2485 unsafe { contraction_builder.launch(contraction_cfg) }
2488 .map_err(|e| format!("apply penalty to direction it={it}: {e}"))?;
2489 let selection_cfg = LaunchConfig {
2490 grid_dim: (1, 1, 1),
2491 block_dim: (1, 1, 1),
2492 shared_mem_bytes: 0,
2493 };
2494 let mut builder = ws.stream.launch_builder(&select_alpha_func);
2495 builder.arg(&loop_ws.alpha_ladder.objective_dev);
2496 builder.arg(&loop_ws.status_u32_dev);
2497 builder.arg(&loop_ws.beta_dev);
2498 builder.arg(&loop_ws.direction_dev);
2499 builder.arg(&ws.beta_orig_dev);
2500 builder.arg(&loop_ws.penalty_direction_dev);
2501 builder.arg(&loop_ws.linear_shift_dev);
2502 builder.arg(&loop_ws.scalar_dev);
2503 builder.arg(&prev_deviance);
2504 builder.arg(&prev_objective);
2505 builder.arg(&constant_shift);
2506 builder.arg(&lm_ridge);
2507 builder.arg(&p_i);
2508 builder.arg(&mut loop_ws.alpha_selection_dev);
2509 unsafe { builder.launch(selection_cfg) }
2513 .map_err(|e| format!("select alpha on device it={it}: {e}"))?;
2514 let selection = ws
2515 .stream
2516 .clone_dtoh(&loop_ws.alpha_selection_dev)
2517 .map_err(|e| format!("download alpha selection it={it}: {e}"))?;
2518 let alpha = selection[0];
2519 let accepted_dev = selection[1];
2520 let accepted_objective = selection[2];
2521 let halving_count = selection[3] as usize;
2522 let dir_linf = selection[4];
2523 let all_candidates_refused = selection[7] != 0.0;
2524 if alpha == 0.0 {
2525 if all_candidates_refused {
2526 let row = selection[5] as usize;
2527 let code = selection[6] as u32;
2528 let eta_host = ws
2529 .stream
2530 .clone_dtoh(&loop_ws.eta_dev)
2531 .map_err(|error| format!("ladder refusal eta download: {error}"))?;
2532 let xd_host = ws
2533 .stream
2534 .clone_dtoh(&loop_ws.xd_dev)
2535 .map_err(|error| format!("ladder refusal direction download: {error}"))?;
2536 let y_host = ws
2537 .stream
2538 .clone_dtoh(&shared.y_dev)
2539 .map_err(|error| format!("ladder refusal response download: {error}"))?;
2540 let prior_host =
2541 ws.stream.clone_dtoh(&shared.prior_w_dev).map_err(|error| {
2542 format!("ladder refusal prior-weight download: {error}")
2543 })?;
2544 let trial_eta = eta_host[row]
2545 + crate::gpu_kernels::pirls_row::ALPHA_LADDER[0] * xd_host[row];
2546 return Err(replay_row_refusal(
2547 family,
2548 curvature,
2549 gamma_shape,
2550 row,
2551 code,
2552 trial_eta,
2553 y_host[row],
2554 prior_host[row],
2555 ));
2556 }
2557 step_search_exhausted = true;
2576 last_halving = 0;
2577 last_step_size = 0.0;
2578 last_dev_delta = 0.0;
2579 break;
2580 }
2581 step_search_exhausted = false;
2582 axpy(
2584 &ws.stream,
2585 &axpy_func,
2586 alpha,
2587 &loop_ws.direction_dev,
2588 &mut loop_ws.beta_dev,
2589 p,
2590 )?;
2591 axpy(
2592 &ws.stream,
2593 &axpy_func,
2594 alpha,
2595 &loop_ws.xd_dev,
2596 &mut loop_ws.eta_dev,
2597 n,
2598 )?;
2599 crate::gpu_kernels::pirls_row::launch_solve_row_on_stream(
2601 backend,
2602 family,
2603 curvature,
2604 gamma_shape,
2605 &ws.stream,
2606 n,
2607 &loop_ws.eta_dev,
2608 &shared.y_dev,
2609 &shared.prior_w_dev,
2610 &mut loop_ws.row_solve,
2611 )
2612 .map_err(|e| format!("solve-row accepted it={it}: {e}"))?;
2613 certify_device_rows(
2614 &ws.stream,
2615 &status_first_func,
2616 &loop_ws.row_solve.status,
2617 &mut loop_ws.status_u32_dev,
2618 family,
2619 curvature,
2620 gamma_shape,
2621 &loop_ws.eta_dev,
2622 &shared.y_dev,
2623 &shared.prior_w_dev,
2624 n,
2625 "solve-row accepted",
2626 )?;
2627
2628 let step_norm = alpha.abs() * dir_linf;
2629 let dev_delta = (prev_objective - accepted_objective).abs();
2630 last_dev_delta = dev_delta;
2631 last_halving = halving_count;
2632 last_step_size = alpha;
2633 if accepted_dev < min_dev {
2634 min_dev = accepted_dev;
2635 }
2636
2637 prev_deviance = accepted_dev;
2638 prev_objective = accepted_objective;
2639
2640 if dir_linf <= tol
2641 && step_norm <= tol
2642 && dev_delta <= tol * (1.0 + prev_objective.abs())
2643 {
2644 converged = true;
2645 crate::gpu_kernels::pirls_row::launch_row_reweight_on_stream(
2647 backend,
2648 family,
2649 curvature,
2650 gamma_shape,
2651 &ws.stream,
2652 n,
2653 &loop_ws.eta_dev,
2654 &shared.y_dev,
2655 &shared.prior_w_dev,
2656 &mut loop_ws.row_final,
2657 )
2658 .map_err(|e| format!("final-row converged: {e}"))?;
2659 certify_device_rows(
2660 &ws.stream,
2661 &status_first_func,
2662 &loop_ws.row_final.status,
2663 &mut loop_ws.status_u32_dev,
2664 family,
2665 curvature,
2666 gamma_shape,
2667 &loop_ws.eta_dev,
2668 &shared.y_dev,
2669 &shared.prior_w_dev,
2670 n,
2671 "final-row converged",
2672 )?;
2673 let h_final = rebuild_h_final(
2674 shared,
2675 ws,
2676 &loop_ws.row_final.w_hessian,
2677 penalty_hessian,
2678 objective_ridge,
2679 )
2680 .map_err(|e| format!("rebuild H_final (converged): {e}"))?;
2681 return build_loop_outcome(
2682 ws,
2683 loop_ws,
2684 h_final,
2685 last_logdet,
2686 prev_deviance,
2687 it + 1,
2688 converged,
2689 lm_ridge,
2690 objective_ridge,
2691 extra,
2692 LoopDiagnostics {
2693 last_deviance_change: last_dev_delta,
2694 last_step_halving: last_halving,
2695 last_step_size,
2696 min_deviance: min_dev,
2697 step_search_exhausted,
2698 },
2699 );
2700 }
2701 }
2702
2703 crate::gpu_kernels::pirls_row::launch_row_reweight_on_stream(
2705 backend,
2706 family,
2707 curvature,
2708 gamma_shape,
2709 &ws.stream,
2710 n,
2711 &loop_ws.eta_dev,
2712 &shared.y_dev,
2713 &shared.prior_w_dev,
2714 &mut loop_ws.row_final,
2715 )
2716 .map_err(|e| format!("final-row max_iter: {e}"))?;
2717 certify_device_rows(
2718 &ws.stream,
2719 &status_first_func,
2720 &loop_ws.row_final.status,
2721 &mut loop_ws.status_u32_dev,
2722 family,
2723 curvature,
2724 gamma_shape,
2725 &loop_ws.eta_dev,
2726 &shared.y_dev,
2727 &shared.prior_w_dev,
2728 n,
2729 "final-row max_iter",
2730 )?;
2731 let h_final = rebuild_h_final(
2732 shared,
2733 ws,
2734 &loop_ws.row_final.w_hessian,
2735 penalty_hessian,
2736 objective_ridge,
2737 )
2738 .map_err(|e| format!("rebuild H_final (max_iter): {e}"))?;
2739 build_loop_outcome(
2740 ws,
2741 loop_ws,
2742 h_final,
2743 last_logdet,
2744 prev_deviance,
2745 max_iter,
2746 converged,
2747 lm_ridge,
2748 objective_ridge,
2749 extra,
2750 LoopDiagnostics {
2751 last_deviance_change: last_dev_delta,
2752 last_step_halving: last_halving,
2753 last_step_size,
2754 min_deviance: min_dev,
2755 step_search_exhausted,
2756 },
2757 )
2758 }
2759
2760 struct LoopDiagnostics {
2773 last_deviance_change: f64,
2774 last_step_halving: usize,
2775 last_step_size: f64,
2776 min_deviance: f64,
2777 step_search_exhausted: bool,
2778 }
2779
2780 fn build_loop_outcome(
2795 ws: &mut SigmaPirlsGpuWorkspace,
2796 loop_ws: &mut PirlsLoopWorkspace,
2797 penalized_hessian: Array2<f64>,
2798 logdet: f64,
2799 deviance: f64,
2800 iterations: usize,
2801 converged: bool,
2802 step_lm_lambda: f64,
2803 objective_ridge: f64,
2804 extra: Option<&PirlsLoopExtra<'_>>,
2805 diagnostics: LoopDiagnostics,
2806 ) -> Result<PirlsLoopOutcome, PirlsGpuLoopError> {
2807 let beta = download_vec(&ws.stream, &loop_ws.beta_dev)?;
2808 let final_eta = download_vec(&ws.stream, &loop_ws.eta_dev)?;
2809 let final_mu = download_vec(&ws.stream, &loop_ws.row_final.mu)?;
2810 let final_grad_eta = download_vec(&ws.stream, &loop_ws.row_final.grad_eta)?;
2811 let final_w_hessian = download_vec(&ws.stream, &loop_ws.row_final.w_hessian)?;
2812 let final_w_solver = download_vec(&ws.stream, &loop_ws.row_final.w_solver)?;
2813
2814 let eta_finite = final_eta.iter().all(|v| v.is_finite());
2819 let mu_finite = final_mu.iter().all(|v| v.is_finite());
2820 let beta_finite = beta.iter().all(|v| v.is_finite());
2821 let stability_ok = eta_finite && mu_finite && beta_finite;
2822 let status = if !stability_ok {
2823 crate::pirls::PirlsStatus::Unstable
2824 } else if converged {
2825 crate::pirls::PirlsStatus::Converged
2826 } else if diagnostics.step_search_exhausted {
2827 crate::pirls::PirlsStatus::LmStepSearchExhausted
2835 } else {
2836 crate::pirls::PirlsStatus::MaxIterationsReached
2837 };
2838
2839 let default_ridge = gam_problem::RidgePassport::scaled_identity(
2842 objective_ridge,
2843 gam_linalg::RidgePolicy::exact_full_objective(),
2844 )
2845 .map_err(gam_problem::EstimationError::from)?;
2846
2847 let max_abs_eta = final_eta.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
2848
2849 match extra {
2850 Some(ext) => {
2851 let (score_c, score_d, solve_dmu_deta, solve_d2mu_deta2, solve_d3mu_deta3) =
2854 crate::pirls::computeworkingweight_derivatives_from_eta(
2855 ext.likelihood,
2856 ext.inverse_link,
2857 &final_eta,
2858 ext.priorweights,
2859 )
2860 .map_err(PirlsGpuLoopError::Geometry)?;
2861
2862 let (finalweights, solve_c_array, solve_d_array) = match ext.exported_curvature {
2863 crate::pirls::HessianCurvatureKind::Observed => {
2864 crate::pirls::compute_observed_hessian_curvature_arrays(
2865 ext.likelihood,
2866 ext.inverse_link,
2867 &final_eta,
2868 ext.y,
2869 &final_w_solver,
2870 ext.priorweights,
2871 )
2872 .map_err(PirlsGpuLoopError::Geometry)?
2873 }
2874 crate::pirls::HessianCurvatureKind::Fisher => {
2875 (final_w_solver.clone(), score_c.clone(), score_d.clone())
2876 }
2877 };
2878
2879 let beta_transformed = beta.clone();
2885
2886 let constraint_kkt = ext.linear_constraints.and_then(|lin| {
2887 if lin.a.nrows() == 0 {
2888 return None;
2889 }
2890 let grad = penalized_hessian.dot(&beta);
2896 Some(crate::active_set::compute_constraint_kkt_diagnostics(
2897 &beta, &grad, lin,
2898 ))
2899 });
2900
2901 let ridge_passport = ext.ridge_passport.unwrap_or(default_ridge);
2902 let firth = ext
2903 .firth
2904 .clone()
2905 .unwrap_or(crate::pirls::FirthDiagnostics::Inactive);
2906 let edf = ext.edf.unwrap_or(f64::NAN);
2907 let derivatives_unsupported = false;
2913
2914 Ok(PirlsLoopOutcome {
2915 beta,
2916 penalized_hessian,
2917 logdet,
2918 deviance,
2919 iterations,
2920 converged,
2921 final_eta,
2922 final_mu,
2923 final_grad_eta,
2924 final_w_hessian,
2925 final_w_solver: final_w_solver.clone(),
2926 final_offset: ext.offset.to_owned(),
2927 beta_transformed,
2928 finalweights,
2929 solveweights: final_w_solver,
2930 solve_dmu_deta,
2931 solve_d2mu_deta2,
2932 solve_d3mu_deta3,
2933 solve_c_array,
2934 solve_d_array,
2935 derivatives_unsupported,
2936 status,
2937 ridge_passport,
2938 firth,
2939 constraint_kkt,
2940 edf,
2941 last_deviance_change: diagnostics.last_deviance_change,
2942 last_step_halving: diagnostics.last_step_halving,
2943 last_step_size: diagnostics.last_step_size,
2944 final_lm_lambda: step_lm_lambda,
2945 min_deviance: diagnostics.min_deviance,
2946 max_abs_eta,
2947 })
2948 }
2949 None => {
2950 Ok(PirlsLoopOutcome {
2958 beta: beta.clone(),
2959 penalized_hessian,
2960 logdet,
2961 deviance,
2962 iterations,
2963 converged,
2964 final_eta,
2965 final_mu,
2966 final_grad_eta,
2967 final_w_hessian,
2968 final_w_solver: final_w_solver.clone(),
2969 final_offset: Array1::<f64>::zeros(0),
2970 beta_transformed: beta,
2971 finalweights: Array1::<f64>::zeros(0),
2972 solveweights: final_w_solver,
2973 solve_dmu_deta: Array1::<f64>::zeros(0),
2974 solve_d2mu_deta2: Array1::<f64>::zeros(0),
2975 solve_d3mu_deta3: Array1::<f64>::zeros(0),
2976 solve_c_array: Array1::<f64>::zeros(0),
2977 solve_d_array: Array1::<f64>::zeros(0),
2978 derivatives_unsupported: true,
2979 status,
2980 ridge_passport: default_ridge,
2981 firth: crate::pirls::FirthDiagnostics::Inactive,
2982 constraint_kkt: None,
2983 edf: f64::NAN,
2984 last_deviance_change: diagnostics.last_deviance_change,
2985 last_step_halving: diagnostics.last_step_halving,
2986 last_step_size: diagnostics.last_step_size,
2987 final_lm_lambda: step_lm_lambda,
2988 min_deviance: diagnostics.min_deviance,
2989 max_abs_eta,
2990 })
2991 }
2992 }
2993 }
2994
2995 fn gemv_no_trans(
2996 blas: &CudaBlas,
2997 n: usize,
2998 p: usize,
2999 a_dev: &CudaSlice<f64>,
3000 x_dev: &CudaSlice<f64>,
3001 y_dev: &mut CudaSlice<f64>,
3002 ) -> Result<(), String> {
3003 let n_i = to_i32(n)?;
3004 let p_i = to_i32(p)?;
3005 let cfg = GemvConfig::<f64> {
3006 trans: cublasOperation_t::CUBLAS_OP_N,
3007 m: n_i,
3008 n: p_i,
3009 alpha: 1.0,
3010 lda: n_i,
3011 incx: 1,
3012 beta: 0.0,
3013 incy: 1,
3014 };
3015 unsafe { blas.gemv(cfg, a_dev, x_dev, y_dev) }.map_err(|e| format!("dgemv no-trans: {e}"))
3017 }
3018
3019 fn axpy(
3020 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3021 func: &cudarc::driver::CudaFunction,
3022 alpha: f64,
3023 x_dev: &CudaSlice<f64>,
3024 y_dev: &mut CudaSlice<f64>,
3025 n: usize,
3026 ) -> Result<(), String> {
3027 const THREADS: u32 = 256;
3028 let n_i = to_i32(n)?;
3029 let n_u = u32::try_from(n).map_err(|_| format!("axpy n={n} > u32"))?;
3030 let grid = n_u.div_ceil(THREADS).max(1);
3031 let cfg = LaunchConfig {
3032 grid_dim: (grid, 1, 1),
3033 block_dim: (THREADS, 1, 1),
3034 shared_mem_bytes: 0,
3035 };
3036 let mut builder = stream.launch_builder(func);
3037 builder.arg(&alpha);
3038 builder.arg(x_dev);
3039 builder.arg(y_dev);
3040 builder.arg(&n_i);
3041 unsafe { builder.launch(cfg) }.map_err(|e| format!("axpy launch: {e}"))?;
3044 Ok(())
3045 }
3046
3047 fn launch_scalar_reduction(
3048 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3049 func: &cudarc::driver::CudaFunction,
3050 src: &CudaSlice<f64>,
3051 len: usize,
3052 scalar_dev: &mut CudaSlice<f64>,
3053 label: &'static str,
3054 ) -> Result<(), String> {
3055 const THREADS: u32 = 1024;
3056 let len_i = to_i32(len)?;
3057 let cfg = LaunchConfig {
3058 grid_dim: (1, 1, 1),
3059 block_dim: (THREADS, 1, 1),
3060 shared_mem_bytes: 0,
3061 };
3062 let mut builder = stream.launch_builder(func);
3063 builder.arg(src);
3064 builder.arg(&len_i);
3065 builder.arg(&mut *scalar_dev);
3066 unsafe { builder.launch(cfg) }.map_err(|e| format!("{label} reduce launch: {e}"))?;
3070 Ok(())
3071 }
3072
3073 fn reduce_scalar(
3074 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3075 func: &cudarc::driver::CudaFunction,
3076 src: &CudaSlice<f64>,
3077 len: usize,
3078 scalar_dev: &mut CudaSlice<f64>,
3079 label: &'static str,
3080 ) -> Result<f64, String> {
3081 launch_scalar_reduction(stream, func, src, len, scalar_dev, label)?;
3082 let host = stream
3083 .clone_dtoh(scalar_dev)
3084 .map_err(|e| format!("download {label}: {e}"))?;
3085 Ok(host[0])
3086 }
3087
3088 fn reduce_status_first(
3091 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3092 func: &cudarc::driver::CudaFunction,
3093 src: &CudaSlice<u32>,
3094 len: usize,
3095 status_dev: &mut CudaSlice<u32>,
3096 label: &'static str,
3097 ) -> Result<Option<(usize, u32)>, String> {
3098 const THREADS: u32 = 1024;
3099 let len_i = to_i32(len)?;
3100 let cfg = LaunchConfig {
3101 grid_dim: (1, 1, 1),
3102 block_dim: (THREADS, 1, 1),
3103 shared_mem_bytes: 0,
3104 };
3105 let mut builder = stream.launch_builder(func);
3106 builder.arg(src);
3107 builder.arg(&len_i);
3108 builder.arg(&mut *status_dev);
3109 unsafe { builder.launch(cfg) }.map_err(|e| format!("{label} first reduce launch: {e}"))?;
3112 let host = stream
3113 .clone_dtoh(status_dev)
3114 .map_err(|e| format!("download {label}: {e}"))?;
3115 if host[0] == u32::MAX {
3116 Ok(None)
3117 } else {
3118 Ok(Some((host[0] as usize, host[1])))
3119 }
3120 }
3121
3122 fn launch_ladder_status_first_reduction(
3125 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3126 func: &cudarc::driver::CudaFunction,
3127 src: &CudaSlice<u32>,
3128 n: usize,
3129 status_dev: &mut CudaSlice<u32>,
3130 ) -> Result<(), String> {
3131 const THREADS: u32 = 1024;
3132 let n_i = to_i32(n)?;
3133 let cfg = LaunchConfig {
3134 grid_dim: (crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN as u32, 1, 1),
3135 block_dim: (THREADS, 1, 1),
3136 shared_mem_bytes: 0,
3137 };
3138 let mut builder = stream.launch_builder(func);
3139 builder.arg(src);
3140 builder.arg(&n_i);
3141 builder.arg(&mut *status_dev);
3142 unsafe { builder.launch(cfg) }
3145 .map_err(|e| format!("alpha-ladder status reduction launch: {e}"))?;
3146 Ok(())
3147 }
3148
3149 fn replay_row_refusal(
3150 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3151 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3152 gamma_shape: f64,
3153 row: usize,
3154 code: u32,
3155 eta: f64,
3156 y: f64,
3157 prior_weight: f64,
3158 ) -> PirlsGpuLoopError {
3159 let input = crate::gpu_kernels::pirls_row::RowInput {
3160 eta,
3161 y,
3162 prior_weight,
3163 };
3164 match crate::gpu_kernels::pirls_row::row_reweight_cpu_at(
3165 row,
3166 family,
3167 curvature,
3168 input,
3169 gamma_shape,
3170 ) {
3171 Err(error) => PirlsGpuLoopError::Geometry(error),
3172 Ok(_) => PirlsGpuLoopError::Geometry(
3173 gam_problem::EstimationError::PirlsRowGeometryUnrepresentable {
3174 row,
3175 quantity: crate::gpu_kernels::pirls_row::status_codes::quantity(code),
3176 eta,
3177 value: f64::from(code),
3178 },
3179 ),
3180 }
3181 }
3182
3183 fn certify_device_rows(
3184 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3185 status_first_func: &cudarc::driver::CudaFunction,
3186 status: &CudaSlice<u32>,
3187 status_scratch: &mut CudaSlice<u32>,
3188 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3189 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3190 gamma_shape: f64,
3191 eta: &CudaSlice<f64>,
3192 y: &CudaSlice<f64>,
3193 prior_weight: &CudaSlice<f64>,
3194 n: usize,
3195 label: &'static str,
3196 ) -> Result<(), PirlsGpuLoopError> {
3197 let Some((_row, _code)) =
3198 reduce_status_first(stream, status_first_func, status, n, status_scratch, label)?
3199 else {
3200 return Ok(());
3201 };
3202 let eta_host = stream
3203 .clone_dtoh(eta)
3204 .map_err(|error| format!("{label} refusal eta download: {error}"))?;
3205 let y_host = stream
3206 .clone_dtoh(y)
3207 .map_err(|error| format!("{label} refusal response download: {error}"))?;
3208 let prior_host = stream
3209 .clone_dtoh(prior_weight)
3210 .map_err(|error| format!("{label} refusal prior-weight download: {error}"))?;
3211 let status_host = stream
3212 .clone_dtoh(status)
3213 .map_err(|error| format!("{label} refusal status download: {error}"))?;
3214 crate::gpu_kernels::pirls_row::replay_first_refusal(
3215 family,
3216 curvature,
3217 gamma_shape,
3218 &eta_host,
3219 &y_host,
3220 &prior_host,
3221 &status_host,
3222 )
3223 .map_err(PirlsGpuLoopError::Geometry)
3224 }
3225
3226 fn download_vec(
3227 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3228 dev: &CudaSlice<f64>,
3229 ) -> Result<Array1<f64>, String> {
3230 let host = stream
3231 .clone_dtoh(dev)
3232 .map_err(|e| format!("download vec: {e}"))?;
3233 Ok(Array1::from_vec(host))
3234 }
3235
3236 pub struct GaussianPlsResult {
3238 pub beta: Array1<f64>,
3239 pub penalized_hessian: Array2<f64>,
3240 pub logdet: f64,
3241 }
3242
3243 pub fn solve_gaussian_pls_on_stream(
3246 a_orig: ArrayView2<'_, f64>,
3247 b_orig: ArrayView1<'_, f64>,
3248 s_transformed: ArrayView2<'_, f64>,
3249 linear_shift: ArrayView1<'_, f64>,
3250 prior_mean_target: ArrayView1<'_, f64>,
3251 ridge: f64,
3252 qs: Option<ArrayView2<'_, f64>>,
3253 ) -> Result<GaussianPlsResult, String> {
3254 let p = b_orig.len();
3255 if a_orig.dim() != (p, p) {
3256 return Err(format!("A shape {:?} != ({p},{p})", a_orig.dim()));
3257 }
3258 if s_transformed.dim() != (p, p) {
3259 return Err(format!("S shape {:?} != ({p},{p})", s_transformed.dim()));
3260 }
3261 if linear_shift.len() != p {
3262 return Err(format!("linear_shift len {} != p={p}", linear_shift.len()));
3263 }
3264 if prior_mean_target.len() != p {
3265 return Err(format!(
3266 "prior_mean_target len {} != p={p}",
3267 prior_mean_target.len()
3268 ));
3269 }
3270 if let Some(qs_v) = qs {
3271 if qs_v.dim() != (p, p) {
3272 return Err(format!("qs shape {:?} != ({p},{p})", qs_v.dim()));
3273 }
3274 }
3275 let (h_rotated, rhs_base) = if let Some(qs_v) = qs {
3276 let qs_owned = qs_v.to_owned();
3277 let tmp = a_orig.dot(&qs_owned);
3278 let h = qs_owned.t().dot(&tmp);
3279 let rb = qs_owned.t().dot(&b_orig);
3280 (h, rb)
3281 } else {
3282 (a_orig.to_owned(), b_orig.to_owned())
3283 };
3284 let penalized_hessian: Array2<f64> = &h_rotated + &s_transformed;
3285 let mut regularized = penalized_hessian.clone();
3286 if ridge > 0.0 {
3287 for i in 0..p {
3288 regularized[[i, i]] += ridge;
3289 }
3290 }
3291 let mut rhs_host = rhs_base;
3292 rhs_host += &linear_shift;
3293 if ridge > 0.0 {
3294 rhs_host.scaled_add(ridge, &prior_mean_target);
3295 }
3296 let (ctx, stream) = context_and_stream()?;
3297 let solver = DnHandle::new(stream.clone())
3298 .map_err(|e| format!("cusolver init (gaussian pls): {e}"))?;
3299 let pp = p.checked_mul(p).ok_or("p*p overflow (gaussian pls)")?;
3300 let mut h_dev = stream
3301 .alloc_zeros::<f64>(pp)
3302 .map_err(|e| format!("alloc H (gaussian pls): {e}"))?;
3303 let mut rhs_dev = stream
3304 .alloc_zeros::<f64>(p)
3305 .map_err(|e| format!("alloc rhs (gaussian pls): {e}"))?;
3306 let potrf_lwork_usize = potrf_query_lwork(&solver, &stream, p)?;
3307 let potrf_lwork = i32::try_from(potrf_lwork_usize)
3308 .map_err(|_| "potrf lwork overflow (gaussian pls)".to_string())?;
3309 let mut potrf_work_dev = stream
3310 .alloc_zeros::<f64>(potrf_lwork_usize.max(1))
3311 .map_err(|e| format!("alloc potrf workspace (gaussian pls): {e}"))?;
3312 let mut potrf_info_dev = stream
3313 .alloc_zeros::<i32>(1)
3314 .map_err(|e| format!("alloc potrf info (gaussian pls): {e}"))?;
3315 let mut potrs_info_dev = stream
3316 .alloc_zeros::<i32>(1)
3317 .map_err(|e| format!("alloc potrs info (gaussian pls): {e}"))?;
3318 let reg_col = to_col_major(®ularized);
3319 stream
3320 .memcpy_htod(reg_col.as_ref(), &mut h_dev)
3321 .map_err(|e| format!("upload H (gaussian pls): {e}"))?;
3322 let rhs_slice = rhs_host
3323 .as_slice()
3324 .ok_or("rhs_host not contiguous (gaussian pls)")?;
3325 stream
3326 .memcpy_htod(rhs_slice, &mut rhs_dev)
3327 .map_err(|e| format!("upload rhs (gaussian pls): {e}"))?;
3328 potrf_in_place_reuse(
3329 &solver,
3330 &stream,
3331 p,
3332 potrf_lwork,
3333 &mut h_dev,
3334 &mut potrf_work_dev,
3335 &mut potrf_info_dev,
3336 )?;
3337 potrs_in_place_reuse(
3338 &solver,
3339 &stream,
3340 p,
3341 1,
3342 &h_dev,
3343 &mut rhs_dev,
3344 &mut potrs_info_dev,
3345 )?;
3346 let logdet = cholesky_logdet_device(&stream, &ctx, p, &h_dev)?;
3347 let beta_raw = stream
3348 .clone_dtoh(&rhs_dev)
3349 .map_err(|e| format!("download beta (gaussian pls): {e}"))?;
3350 check_deferred_potrf_info(&stream, &potrf_info_dev)?;
3351 check_deferred_potrs_info(&stream, &potrs_info_dev)?;
3352 Ok(GaussianPlsResult {
3353 beta: Array1::from_vec(beta_raw),
3354 penalized_hessian,
3355 logdet,
3356 })
3357 }
3358}
3359
3360pub fn weighted_crossprod_gpu(
3361 x: ArrayView2<'_, f64>,
3362 weights: ArrayView1<'_, f64>,
3363) -> Result<Array2<f64>, String> {
3364 #[cfg(not(target_os = "linux"))]
3365 {
3366 return cpu_fallback::weighted_crossprod_cpu(x, weights);
3367 }
3368
3369 #[cfg(target_os = "linux")]
3370 {
3371 if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
3372 .map_err(|error| error.to_string())?
3373 .is_none()
3374 {
3375 return cpu_fallback::weighted_crossprod_cpu(x, weights);
3376 }
3377 cuda::weighted_crossprod(x, weights)
3378 }
3379}
3380
3381pub fn solve_pirls_step_gpu(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
3382 #[cfg(not(target_os = "linux"))]
3383 {
3384 return cpu_fallback::solve_step_cpu(input);
3385 }
3386
3387 #[cfg(target_os = "linux")]
3388 {
3389 if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
3390 .map_err(|error| error.to_string())?
3391 .is_none()
3392 {
3393 return cpu_fallback::solve_step_cpu(input);
3394 }
3395 cuda::solve_step(input)
3396 }
3397}
3398
3399#[cfg(target_os = "linux")]
3405pub fn upload_shared_pirls_gpu(
3406 x: ndarray::ArrayView2<'_, f64>,
3407 y: ndarray::ArrayView1<'_, f64>,
3408 prior_w: ndarray::ArrayView1<'_, f64>,
3409 offset: ndarray::ArrayView1<'_, f64>,
3410) -> Result<PirlsGpuSharedData, String> {
3411 gam_gpu::device_runtime::GpuRuntime::require()
3412 .map_err(|error| format!("cannot upload shared GPU PIRLS data: {error}"))?;
3413 PirlsGpuSharedData::upload_impl(x, y, prior_w, offset)
3414}
3415
3416#[cfg(target_os = "linux")]
3420pub fn allocate_sigma_pirls_workspace(
3421 shared: &PirlsGpuSharedData,
3422) -> Result<SigmaPirlsGpuWorkspace, String> {
3423 SigmaPirlsGpuWorkspace::allocate_impl(shared)
3424}
3425
3426#[cfg(target_os = "linux")]
3431pub fn upload_qs_pirls(
3432 ws: &mut SigmaPirlsGpuWorkspace,
3433 qs: ndarray::ArrayView2<'_, f64>,
3434) -> Result<(), String> {
3435 cuda::upload_qs(ws, qs)
3436}
3437
3438#[cfg(target_os = "linux")]
3441pub fn upload_qs_identity_pirls(ws: &mut SigmaPirlsGpuWorkspace) -> Result<(), String> {
3442 cuda::upload_qs_identity(ws)
3443}
3444
3445#[cfg(target_os = "linux")]
3455pub(crate) fn pirls_loop_on_stream(
3456 shared: &PirlsGpuSharedData,
3457 ws: &mut SigmaPirlsGpuWorkspace,
3458 loop_ws: &mut cuda::PirlsLoopWorkspace,
3459 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3460 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3461 likelihood_scale: PirlsLoopLikelihoodScale,
3462 beta0: ndarray::ArrayView1<'_, f64>,
3463 penalty_hessian: ndarray::ArrayView2<'_, f64>,
3464 linear_shift: ndarray::ArrayView1<'_, f64>,
3467 constant_shift: f64,
3469 step_lm_lambda: f64,
3470 objective_ridge: f64,
3471 max_iter: usize,
3472 tol: f64,
3473 extra: Option<&cuda::PirlsLoopExtra<'_>>,
3474) -> Result<cuda::PirlsLoopOutcome, cuda::PirlsGpuLoopError> {
3475 let gamma_shape = likelihood_scale
3476 .kernel_argument(family)
3477 .map_err(cuda::PirlsGpuLoopError::Runtime)?;
3478 cuda::pirls_loop(
3479 shared,
3480 ws,
3481 loop_ws,
3482 family,
3483 curvature,
3484 gamma_shape,
3485 beta0,
3486 penalty_hessian,
3487 linear_shift,
3488 constant_shift,
3489 step_lm_lambda,
3490 objective_ridge,
3491 max_iter,
3492 tol,
3493 extra,
3494 )
3495}
3496
3497#[cfg(target_os = "linux")]
3500pub fn allocate_pirls_loop_workspace(
3501 shared: &PirlsGpuSharedData,
3502 ws: &SigmaPirlsGpuWorkspace,
3503) -> Result<cuda::PirlsLoopWorkspace, String> {
3504 cuda::PirlsLoopWorkspace::allocate(shared, &ws.stream)
3505}
3506
3507#[cfg(target_os = "linux")]
3513pub fn solve_gaussian_pls_gpu(
3514 a_orig: ndarray::ArrayView2<'_, f64>,
3515 b_orig: ndarray::ArrayView1<'_, f64>,
3516 s_transformed: ndarray::ArrayView2<'_, f64>,
3517 linear_shift: ndarray::ArrayView1<'_, f64>,
3518 prior_mean_target: ndarray::ArrayView1<'_, f64>,
3519 ridge: f64,
3520 qs: Option<ndarray::ArrayView2<'_, f64>>,
3521) -> Result<cuda::GaussianPlsResult, String> {
3522 cuda::solve_gaussian_pls_on_stream(
3523 a_orig,
3524 b_orig,
3525 s_transformed,
3526 linear_shift,
3527 prior_mean_target,
3528 ridge,
3529 qs,
3530 )
3531}
3532
3533mod cpu_fallback {
3541 use super::{PirlsGpuInput, PirlsGpuStep};
3542 use crate::estimate::reml::assembly::xt_diag_x_dense_into;
3543 use faer::Side;
3544 use gam_linalg::faer_ndarray::FaerCholesky;
3545 use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
3546
3547 pub(super) fn weighted_crossprod_cpu(
3548 x: ArrayView2<'_, f64>,
3549 weights: ArrayView1<'_, f64>,
3550 ) -> Result<Array2<f64>, String> {
3551 validate(x, weights)?;
3552 let x_owned = x.to_owned();
3553 let w_owned = weights.to_owned();
3554 let mut scratch = Array2::<f64>::zeros(x_owned.dim());
3555 Ok(xt_diag_x_dense_into(&x_owned, &w_owned, &mut scratch))
3556 }
3557
3558 pub(super) fn solve_step_cpu(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
3559 validate(input.x, input.weights)?;
3560 let (_n, p) = input.x.dim();
3561 if input.penalty_hessian.dim() != (p, p) {
3562 return Err(format!(
3563 "penalty Hessian shape {:?} does not match p={p}",
3564 input.penalty_hessian.dim()
3565 ));
3566 }
3567 if input.gradient.len() != p {
3568 return Err(format!(
3569 "gradient length {} does not match p={p}",
3570 input.gradient.len()
3571 ));
3572 }
3573 let xtwx = weighted_crossprod_cpu(input.x, input.weights)?;
3574 let mut penalized_hessian = xtwx.clone();
3576 penalized_hessian += &input.penalty_hessian;
3577 if input.objective_ridge != 0.0 {
3578 for i in 0..p {
3579 penalized_hessian[[i, i]] += input.objective_ridge;
3580 }
3581 }
3582 let mut h_step = xtwx;
3584 h_step += &input.penalty_hessian;
3585 if input.step_lm_lambda != 0.0 {
3586 for i in 0..p {
3587 h_step[[i, i]] += input.step_lm_lambda;
3588 }
3589 }
3590 let factor = h_step
3591 .cholesky(Side::Lower)
3592 .map_err(|e| format!("CPU Cholesky failed in PIRLS fallback: {e:?}"))?;
3593 let g = Array1::from_iter(input.gradient.iter().copied());
3594 let direction = factor.solvevec(&g);
3597 let logdet = 2.0 * factor.diag().iter().map(|v| v.ln()).sum::<f64>();
3599 Ok(PirlsGpuStep {
3600 penalized_hessian,
3601 direction,
3602 logdet,
3603 })
3604 }
3605
3606 fn validate(x: ArrayView2<'_, f64>, weights: ArrayView1<'_, f64>) -> Result<(), String> {
3607 let (n, p) = x.dim();
3608 if weights.len() != n {
3609 return Err(format!(
3610 "weights length {} does not match rows {n}",
3611 weights.len()
3612 ));
3613 }
3614 if n == 0 || p == 0 {
3615 return Err("empty design cannot be solved".to_string());
3616 }
3617 Ok(())
3618 }
3619}
3620
3621pub fn cholesky_solve_gpu(
3622 hessian: ArrayView2<'_, f64>,
3623 rhs: ArrayView2<'_, f64>,
3624) -> Result<(Array2<f64>, f64), String> {
3625 gam_gpu::solver::cholesky_solve_gpu(hessian, rhs)
3626}
3627
3628pub fn cholesky_solve_only_gpu(
3632 hessian: ArrayView2<'_, f64>,
3633 rhs: ArrayView2<'_, f64>,
3634) -> Result<Array2<f64>, String> {
3635 gam_gpu::solver::cholesky_solve_only_gpu(hessian, rhs)
3636}
3637
3638#[cfg(all(test, target_os = "linux"))]
3639mod pirls_loop_likelihood_scale_tests {
3640 use super::PirlsLoopLikelihoodScale;
3641 use crate::gpu_kernels::pirls_row::PirlsRowFamily;
3642
3643 #[test]
3644 fn gpu_row_scale_discriminant_rejects_family_mismatch() {
3645 assert!(
3646 PirlsLoopLikelihoodScale::non_gamma()
3647 .kernel_argument(PirlsRowFamily::GammaLog)
3648 .is_err()
3649 );
3650 let gamma = PirlsLoopLikelihoodScale::gamma_shape(2.0).expect("positive Gamma shape");
3651 assert!(gamma.kernel_argument(PirlsRowFamily::PoissonLog).is_err());
3652 assert_eq!(
3653 gamma
3654 .kernel_argument(PirlsRowFamily::GammaLog)
3655 .expect("matching Gamma contract"),
3656 2.0
3657 );
3658 }
3659
3660 #[test]
3661 fn non_gamma_kernel_scalar_is_poisoned_not_unit_scaled() {
3662 let abi_value = PirlsLoopLikelihoodScale::non_gamma()
3663 .kernel_argument(PirlsRowFamily::PoissonLog)
3664 .expect("matching non-Gamma contract");
3665 assert!(abi_value.is_nan());
3666 }
3667}
3668
3669#[cfg(test)]
3678mod weighted_crossprod_cpu_fallback_tests {
3679 use super::weighted_crossprod_gpu;
3680 use ndarray::{Array1, Array2};
3681
3682 #[test]
3683 fn weighted_crossprod_gpu_cpu_fallback_matches_dense_xtwx() {
3684 let x = Array2::<f64>::from_shape_fn((4, 3), |(i, j)| (i + j) as f64 + 1.0);
3687 let w = Array1::<f64>::from_vec(vec![0.5, 1.0, 1.5, 2.0]);
3688
3689 let got = weighted_crossprod_gpu(x.view(), w.view())
3690 .expect("weighted_crossprod_gpu must return Ok via CPU fallback on a CPU-only host");
3691
3692 let (n, p) = x.dim();
3694 let mut expected = Array2::<f64>::zeros((p, p));
3695 for k in 0..n {
3696 for i in 0..p {
3697 for j in 0..p {
3698 expected[[i, j]] += w[k] * x[[k, i]] * x[[k, j]];
3699 }
3700 }
3701 }
3702
3703 assert_eq!(got.dim(), (p, p));
3704 for i in 0..p {
3705 for j in 0..p {
3706 let diff = (got[[i, j]] - expected[[i, j]]).abs();
3707 assert!(
3708 diff <= 1e-10,
3709 "XtWX[{i},{j}] mismatch: got vs expected diff={diff}"
3710 );
3711 }
3712 }
3713 }
3714}