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(
880 shared: &PirlsGpuSharedData,
881 ws: &mut SigmaPirlsGpuWorkspace,
882 input: PirlsStepStreamDeviceInput<'_, '_>,
883 ) -> Result<PirlsGpuStep, String> {
884 let n = shared.n;
885 let p = shared.p;
886 if ws.n != n || ws.p != p {
887 return Err(format!(
888 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
889 ws.n, ws.p
890 ));
891 }
892 if input.w_solver_dev.len() != n {
893 return Err(format!(
894 "w_solver_dev length {} does not match n={n}",
895 input.w_solver_dev.len()
896 ));
897 }
898 if input.grad_eta_dev.len() != n {
899 return Err(format!(
900 "grad_eta_dev length {} does not match n={n}",
901 input.grad_eta_dev.len()
902 ));
903 }
904 if input.penalty_hessian.dim() != (p, p) {
905 return Err(format!(
906 "penalty Hessian shape {:?} does not match p={p}",
907 input.penalty_hessian.dim()
908 ));
909 }
910
911 let n_i = to_i32(n)?;
914 let p_i = to_i32(p)?;
915 if let Some(ref mut wx_dev_fb) = ws.wx_dev {
916 left_scale_rows_borrowed(
918 &ws.blas,
919 &ws.stream,
920 n,
921 p,
922 &shared.x_original_dev,
923 input.w_solver_dev,
924 wx_dev_fb,
925 )?;
926 let gemm_cfg = GemmConfig::<f64> {
927 transa: cublasOperation_t::CUBLAS_OP_T,
928 transb: cublasOperation_t::CUBLAS_OP_N,
929 m: p_i,
930 n: p_i,
931 k: n_i,
932 alpha: 1.0,
933 lda: n_i,
934 ldb: n_i,
935 beta: 0.0,
936 ldc: p_i,
937 };
938 unsafe {
941 ws.blas.gemm(
942 gemm_cfg,
943 &shared.x_original_dev,
944 wx_dev_fb,
945 &mut ws.xtwx_dev,
946 )
947 }
948 .map_err(|e| format!("cublas dgemm XtWX (device-input): {e}"))?;
949 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
950 let penalty_step_col = to_col_major(&penalty_step);
951 ws.stream
952 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
953 .map_err(|e| format!("upload penalty (device-input): {e}"))?;
954 {
956 let cfg_aq = GemmConfig::<f64> {
957 transa: cublasOperation_t::CUBLAS_OP_N,
958 transb: cublasOperation_t::CUBLAS_OP_N,
959 m: p_i,
960 n: p_i,
961 k: p_i,
962 alpha: 1.0,
963 lda: p_i,
964 ldb: p_i,
965 beta: 0.0,
966 ldc: p_i,
967 };
968 unsafe {
970 ws.blas
971 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
972 }
973 .map_err(|e| format!("dgemm A·Qs (device-input large-p): {e}"))?;
974 }
975 {
976 let cfg_qt = GemmConfig::<f64> {
977 transa: cublasOperation_t::CUBLAS_OP_T,
978 transb: cublasOperation_t::CUBLAS_OP_N,
979 m: p_i,
980 n: p_i,
981 k: p_i,
982 alpha: 1.0,
983 lda: p_i,
984 ldb: p_i,
985 beta: 0.0,
986 ldc: p_i,
987 };
988 unsafe {
990 ws.blas
991 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
992 }
993 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (device-input large-p): {e}"))?;
994 }
995 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
996 let gemv_cfg = GemvConfig::<f64> {
997 trans: cublasOperation_t::CUBLAS_OP_T,
998 m: n_i,
999 n: p_i,
1000 alpha: 1.0,
1001 lda: n_i,
1002 incx: 1,
1003 beta: 0.0,
1004 incy: 1,
1005 };
1006 unsafe {
1008 ws.blas.gemv(
1009 gemv_cfg,
1010 &shared.x_original_dev,
1011 input.grad_eta_dev,
1012 &mut ws.rhs_dev,
1013 )
1014 }
1015 .map_err(|e| format!("cublas dgemv Xtg (device-input): {e}"))?;
1016 } else {
1017 launch_xtwx_lower(
1019 &ws.stream,
1020 &shared.ctx,
1021 n,
1022 p,
1023 &shared.x_original_dev,
1024 input.w_solver_dev,
1025 &mut ws.xtwx_dev,
1026 )?;
1027 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1028 launch_xtscore(
1029 &ws.stream,
1030 &shared.ctx,
1031 n,
1032 p,
1033 &shared.x_original_dev,
1034 input.grad_eta_dev,
1035 &mut ws.rhs_dev,
1036 )?;
1037 {
1039 let cfg_aq = GemmConfig::<f64> {
1040 transa: cublasOperation_t::CUBLAS_OP_N,
1041 transb: cublasOperation_t::CUBLAS_OP_N,
1042 m: p_i,
1043 n: p_i,
1044 k: p_i,
1045 alpha: 1.0,
1046 lda: p_i,
1047 ldb: p_i,
1048 beta: 0.0,
1049 ldc: p_i,
1050 };
1051 unsafe {
1053 ws.blas
1054 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1055 }
1056 .map_err(|e| format!("dgemm A·Qs (device-input fused): {e}"))?;
1057 }
1058 {
1059 let cfg_qt = GemmConfig::<f64> {
1060 transa: cublasOperation_t::CUBLAS_OP_T,
1061 transb: cublasOperation_t::CUBLAS_OP_N,
1062 m: p_i,
1063 n: p_i,
1064 k: p_i,
1065 alpha: 1.0,
1066 lda: p_i,
1067 ldb: p_i,
1068 beta: 0.0,
1069 ldc: p_i,
1070 };
1071 unsafe {
1073 ws.blas
1074 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1075 }
1076 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (device-input fused): {e}"))?;
1077 }
1078 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
1079 let penalty_step_col = to_col_major(&penalty_step);
1080 ws.stream
1081 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
1082 .map_err(|e| format!("upload penalty (fused device-input): {e}"))?;
1083 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1084 }
1085
1086 {
1091 let cfg_qts = GemvConfig::<f64> {
1093 trans: cublasOperation_t::CUBLAS_OP_T,
1094 m: p_i,
1095 n: p_i,
1096 alpha: 1.0,
1097 lda: p_i,
1098 incx: 1,
1099 beta: 0.0,
1100 incy: 1,
1101 };
1102 unsafe {
1104 ws.blas
1105 .gemv(cfg_qts, &ws.qs_dev, &ws.rhs_dev, &mut ws.beta_orig_dev)
1106 }
1107 .map_err(|e| format!("dgemv Qsᵀ·score (device-input): {e}"))?;
1108 ws.stream
1110 .memcpy_dtod(&ws.beta_orig_dev, &mut ws.rhs_dev)
1111 .map_err(|e| format!("d2d Qsᵀ·score→rhs (device-input): {e}"))?;
1112 let rhs_raw = ws
1114 .stream
1115 .clone_dtoh(&ws.rhs_dev)
1116 .map_err(|e| format!("download Qsᵀscore (device-input): {e}"))?;
1117 let beta_raw = ws
1118 .stream
1119 .clone_dtoh(input.beta_dev)
1120 .map_err(|e| format!("download beta (device-input): {e}"))?;
1121 let mut rhs_host = Array1::from_vec(rhs_raw);
1122 let beta_host = Array1::from_vec(beta_raw);
1123 let s_beta = input.penalty_hessian.dot(&beta_host);
1124 rhs_host -= &s_beta;
1125 rhs_host += &input.linear_shift;
1126 ws.stream
1127 .memcpy_htod(
1128 rhs_host
1129 .as_slice()
1130 .ok_or("rhs_host not contiguous (device-input correction)")?,
1131 &mut ws.rhs_dev,
1132 )
1133 .map_err(|e| format!("re-upload corrected rhs (device-input): {e}"))?;
1134 }
1135
1136 let xtwx_col = ws
1140 .stream
1141 .clone_dtoh(&ws.xtwx_dev)
1142 .map_err(|e| format!("download XᵀWX (device-input): {e}"))?;
1143 let xtwx_host = from_col_major(&xtwx_col, p, p)
1144 .ok_or("XᵀWX layout conversion failed (device-input)")?;
1145 let qs_col = ws
1146 .stream
1147 .clone_dtoh(&ws.qs_dev)
1148 .map_err(|e| format!("download Qs (device-input): {e}"))?;
1149 let qs_host =
1150 from_col_major(&qs_col, p, p).ok_or("Qs layout conversion failed (device-input)")?;
1151 let tmp_aq = xtwx_host.dot(&qs_host);
1152 let h_rotated = qs_host.t().dot(&tmp_aq);
1153 let penalty_export = penalty_with_ridge(input.penalty_hessian, input.objective_ridge);
1154 let penalized_hessian = h_rotated + &penalty_export;
1155
1156 potrf_in_place_reuse(
1159 &ws.solver,
1160 &ws.stream,
1161 p,
1162 ws.potrf_lwork,
1163 &mut ws.h_dev,
1164 &mut ws.potrf_work_dev,
1165 &mut ws.potrf_info_dev,
1166 )?;
1167 potrs_in_place_reuse(
1168 &ws.solver,
1169 &ws.stream,
1170 p,
1171 1,
1172 &ws.h_dev,
1173 &mut ws.rhs_dev,
1174 &mut ws.potrs_info_dev,
1175 )?;
1176
1177 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
1178
1179 let direction_raw = ws
1180 .stream
1181 .clone_dtoh(&ws.rhs_dev)
1182 .map_err(|e| format!("download direction (device-input): {e}"))?;
1183 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
1187 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
1188 let direction = Array1::from_vec(direction_raw);
1191
1192 Ok(PirlsGpuStep {
1193 penalized_hessian,
1194 direction,
1195 logdet,
1196 })
1197 }
1198
1199 pub(super) fn solve_step_on_stream_device_inplace(
1210 shared: &PirlsGpuSharedData,
1211 ws: &mut SigmaPirlsGpuWorkspace,
1212 input: PirlsStepStreamDeviceInput<'_, '_>,
1213 ) -> Result<f64, String> {
1214 let n = shared.n;
1215 let p = shared.p;
1216 if ws.n != n || ws.p != p {
1217 return Err(format!(
1218 "workspace shape ({}, {}) does not match shared design ({n}, {p})",
1219 ws.n, ws.p
1220 ));
1221 }
1222 if input.w_solver_dev.len() != n {
1223 return Err(format!(
1224 "w_solver_dev length {} does not match n={n}",
1225 input.w_solver_dev.len()
1226 ));
1227 }
1228 if input.grad_eta_dev.len() != n {
1229 return Err(format!(
1230 "grad_eta_dev length {} does not match n={n}",
1231 input.grad_eta_dev.len()
1232 ));
1233 }
1234 if input.penalty_hessian.dim() != (p, p) {
1235 return Err(format!(
1236 "penalty Hessian shape {:?} does not match p={p}",
1237 input.penalty_hessian.dim()
1238 ));
1239 }
1240
1241 if input.linear_shift.len() != p {
1242 return Err(format!(
1243 "linear_shift length {} does not match p={p}",
1244 input.linear_shift.len()
1245 ));
1246 }
1247 if input.beta_dev.len() != p {
1248 return Err(format!(
1249 "beta_dev length {} does not match p={p}",
1250 input.beta_dev.len()
1251 ));
1252 }
1253 let n_i = to_i32(n)?;
1254 let p_i = to_i32(p)?;
1255
1256 if let Some(ref mut wx_dev_ib) = ws.wx_dev {
1259 left_scale_rows_borrowed(
1261 &ws.blas,
1262 &ws.stream,
1263 n,
1264 p,
1265 &shared.x_original_dev,
1266 input.w_solver_dev,
1267 wx_dev_ib,
1268 )?;
1269 let cfg_xtx = GemmConfig::<f64> {
1270 transa: cublasOperation_t::CUBLAS_OP_T,
1271 transb: cublasOperation_t::CUBLAS_OP_N,
1272 m: p_i,
1273 n: p_i,
1274 k: n_i,
1275 alpha: 1.0,
1276 lda: n_i,
1277 ldb: n_i,
1278 beta: 0.0,
1279 ldc: p_i,
1280 };
1281 unsafe {
1283 ws.blas
1284 .gemm(cfg_xtx, &shared.x_original_dev, wx_dev_ib, &mut ws.xtwx_dev)
1285 }
1286 .map_err(|e| format!("dgemm XtWX inplace (large-p): {e}"))?;
1287 let cfg_xts = GemvConfig::<f64> {
1288 trans: cublasOperation_t::CUBLAS_OP_T,
1289 m: n_i,
1290 n: p_i,
1291 alpha: 1.0,
1292 lda: n_i,
1293 incx: 1,
1294 beta: 0.0,
1295 incy: 1,
1296 };
1297 unsafe {
1299 ws.blas.gemv(
1300 cfg_xts,
1301 &shared.x_original_dev,
1302 input.grad_eta_dev,
1303 &mut ws.rhs_dev,
1304 )
1305 }
1306 .map_err(|e| format!("dgemv Xᵀ·score inplace (large-p): {e}"))?;
1307 } else {
1308 launch_xtwx_lower(
1310 &ws.stream,
1311 &shared.ctx,
1312 n,
1313 p,
1314 &shared.x_original_dev,
1315 input.w_solver_dev,
1316 &mut ws.xtwx_dev,
1317 )?;
1318 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1319 launch_xtscore(
1320 &ws.stream,
1321 &shared.ctx,
1322 n,
1323 p,
1324 &shared.x_original_dev,
1325 input.grad_eta_dev,
1326 &mut ws.rhs_dev,
1327 )?;
1328 }
1329
1330 {
1333 let cfg_aq = GemmConfig::<f64> {
1334 transa: cublasOperation_t::CUBLAS_OP_N,
1335 transb: cublasOperation_t::CUBLAS_OP_N,
1336 m: p_i,
1337 n: p_i,
1338 k: p_i,
1339 alpha: 1.0,
1340 lda: p_i,
1341 ldb: p_i,
1342 beta: 0.0,
1343 ldc: p_i,
1344 };
1345 unsafe {
1347 ws.blas
1348 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1349 }
1350 .map_err(|e| format!("dgemm A·Qs inplace: {e}"))?;
1351 }
1352 {
1354 let cfg_qt = GemmConfig::<f64> {
1355 transa: cublasOperation_t::CUBLAS_OP_T,
1356 transb: cublasOperation_t::CUBLAS_OP_N,
1357 m: p_i,
1358 n: p_i,
1359 k: p_i,
1360 alpha: 1.0,
1361 lda: p_i,
1362 ldb: p_i,
1363 beta: 0.0,
1364 ldc: p_i,
1365 };
1366 unsafe {
1368 ws.blas
1369 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1370 }
1371 .map_err(|e| format!("dgemm Qsᵀ·A·Qs inplace: {e}"))?;
1372 }
1373 let penalty_step = penalty_with_ridge(input.penalty_hessian, input.step_lm_lambda);
1375 let penalty_step_col = to_col_major(&penalty_step);
1376 ws.stream
1377 .memcpy_htod(penalty_step_col.as_ref(), &mut ws.penalty_dev)
1378 .map_err(|e| format!("upload penalty inplace: {e}"))?;
1379 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1380
1381 {
1385 let cfg_qts = GemvConfig::<f64> {
1386 trans: cublasOperation_t::CUBLAS_OP_T,
1387 m: p_i,
1388 n: p_i,
1389 alpha: 1.0,
1390 lda: p_i,
1391 incx: 1,
1392 beta: 0.0,
1393 incy: 1,
1394 };
1395 unsafe {
1397 ws.blas
1398 .gemv(cfg_qts, &ws.qs_dev, &ws.rhs_dev, &mut ws.beta_orig_dev)
1399 }
1400 .map_err(|e| format!("dgemv Qsᵀ·score inplace: {e}"))?;
1401 ws.stream
1402 .memcpy_dtod(&ws.beta_orig_dev, &mut ws.rhs_dev)
1403 .map_err(|e| format!("d2d Qsᵀ·score→rhs inplace: {e}"))?;
1404 }
1405 ws.stream
1411 .memcpy_htod(
1412 input
1413 .linear_shift
1414 .as_slice()
1415 .ok_or("linear_shift must be contiguous")?,
1416 &mut ws.dir_orig_dev,
1417 )
1418 .map_err(|e| format!("upload linear shift inplace: {e}"))?;
1419 let loop_module = PIRLS_LOOP_CACHE
1420 .get_or_compile(&shared.ctx, "pirls_loop", PIRLS_LOOP_PTX_SOURCE)
1421 .map_err(|e| format!("load rhs-correction module: {e}"))?;
1422 let correction_func = loop_module
1423 .load_function("correct_newton_rhs")
1424 .map_err(|e| format!("load correct_newton_rhs: {e}"))?;
1425 let cfg = LaunchConfig {
1426 grid_dim: ((p as u32).div_ceil(256).max(1), 1, 1),
1427 block_dim: (256, 1, 1),
1428 shared_mem_bytes: 0,
1429 };
1430 let mut builder = ws.stream.launch_builder(&correction_func);
1431 builder.arg(&mut ws.rhs_dev);
1432 builder.arg(&ws.penalty_dev);
1433 builder.arg(input.beta_dev);
1434 builder.arg(&ws.dir_orig_dev);
1435 builder.arg(&input.step_lm_lambda);
1436 builder.arg(&p_i);
1437 builder.arg(&mut ws.beta_orig_dev);
1438 unsafe { builder.launch(cfg) }
1442 .map_err(|e| format!("correct Newton rhs on device: {e}"))?;
1443
1444 potrf_in_place_reuse(
1446 &ws.solver,
1447 &ws.stream,
1448 p,
1449 ws.potrf_lwork,
1450 &mut ws.h_dev,
1451 &mut ws.potrf_work_dev,
1452 &mut ws.potrf_info_dev,
1453 )?;
1454 potrs_in_place_reuse(
1455 &ws.solver,
1456 &ws.stream,
1457 p,
1458 1,
1459 &ws.h_dev,
1460 &mut ws.rhs_dev,
1461 &mut ws.potrs_info_dev,
1462 )?;
1463 let logdet = cholesky_logdet_device(&ws.stream, &shared.ctx, p, &ws.h_dev)?;
1464 check_deferred_potrf_info(&ws.stream, &ws.potrf_info_dev)?;
1465 check_deferred_potrs_info(&ws.stream, &ws.potrs_info_dev)?;
1466
1467 Ok(logdet)
1470 }
1471
1472 pub(super) fn rebuild_h_final(
1480 shared: &PirlsGpuSharedData,
1481 ws: &mut SigmaPirlsGpuWorkspace,
1482 w_hessian_dev: &CudaSlice<f64>,
1483 penalty_hessian: ArrayView2<'_, f64>,
1484 objective_ridge: f64,
1485 ) -> Result<Array2<f64>, String> {
1486 let n = shared.n;
1487 let p = shared.p;
1488
1489 if let Some(ref mut wx_dev_rh) = ws.wx_dev {
1491 left_scale_rows_borrowed(
1493 &ws.blas,
1494 &ws.stream,
1495 n,
1496 p,
1497 &shared.x_original_dev,
1498 w_hessian_dev,
1499 wx_dev_rh,
1500 )?;
1501 let n_i = to_i32(n)?;
1502 let p_i = to_i32(p)?;
1503 let gemm_cfg = GemmConfig::<f64> {
1504 transa: cublasOperation_t::CUBLAS_OP_T,
1505 transb: cublasOperation_t::CUBLAS_OP_N,
1506 m: p_i,
1507 n: p_i,
1508 k: n_i,
1509 alpha: 1.0,
1510 lda: n_i,
1511 ldb: n_i,
1512 beta: 0.0,
1513 ldc: p_i,
1514 };
1515 unsafe {
1518 ws.blas.gemm(
1519 gemm_cfg,
1520 &shared.x_original_dev,
1521 wx_dev_rh,
1522 &mut ws.xtwx_dev,
1523 )
1524 }
1525 .map_err(|e| format!("cublas dgemm XtWX (final H rebuild): {e}"))?;
1526 } else {
1527 launch_xtwx_lower(
1529 &ws.stream,
1530 &shared.ctx,
1531 n,
1532 p,
1533 &shared.x_original_dev,
1534 w_hessian_dev,
1535 &mut ws.xtwx_dev,
1536 )?;
1537 launch_symmetrize_lower(&ws.stream, &shared.ctx, p, &mut ws.xtwx_dev)?;
1538 }
1539
1540 let p_i = to_i32(p)?;
1542 {
1544 let cfg_aq = GemmConfig::<f64> {
1545 transa: cublasOperation_t::CUBLAS_OP_N,
1546 transb: cublasOperation_t::CUBLAS_OP_N,
1547 m: p_i,
1548 n: p_i,
1549 k: p_i,
1550 alpha: 1.0,
1551 lda: p_i,
1552 ldb: p_i,
1553 beta: 0.0,
1554 ldc: p_i,
1555 };
1556 unsafe {
1558 ws.blas
1559 .gemm(cfg_aq, &ws.xtwx_dev, &ws.qs_dev, &mut ws.qs_tmp_dev)
1560 }
1561 .map_err(|e| format!("dgemm A·Qs (final H rebuild): {e}"))?;
1562 }
1563 {
1565 let cfg_qt = GemmConfig::<f64> {
1566 transa: cublasOperation_t::CUBLAS_OP_T,
1567 transb: cublasOperation_t::CUBLAS_OP_N,
1568 m: p_i,
1569 n: p_i,
1570 k: p_i,
1571 alpha: 1.0,
1572 lda: p_i,
1573 ldb: p_i,
1574 beta: 0.0,
1575 ldc: p_i,
1576 };
1577 unsafe {
1579 ws.blas
1580 .gemm(cfg_qt, &ws.qs_dev, &ws.qs_tmp_dev, &mut ws.h_dev)
1581 }
1582 .map_err(|e| format!("dgemm Qsᵀ·A·Qs (final H rebuild): {e}"))?;
1583 }
1584 let penalty = penalty_with_ridge(penalty_hessian, objective_ridge);
1585 let penalty_col = to_col_major(&penalty);
1586 ws.stream
1587 .memcpy_htod(penalty_col.as_ref(), &mut ws.penalty_dev)
1588 .map_err(|e| format!("upload penalty (final H rebuild): {e}"))?;
1589 geam_add_inplace(&ws.blas, &ws.stream, p, &mut ws.h_dev, &ws.penalty_dev)?;
1590
1591 let h_col = ws
1593 .stream
1594 .clone_dtoh(&ws.h_dev)
1595 .map_err(|e| format!("download H_final: {e}"))?;
1596 from_col_major(&h_col, p, p).ok_or_else(|| "H_final layout conversion failed".to_string())
1597 }
1598
1599 pub(super) fn weighted_crossprod(
1600 x: ArrayView2<'_, f64>,
1601 weights: ArrayView1<'_, f64>,
1602 ) -> Result<Array2<f64>, String> {
1603 let (_, stream) = context_and_stream()?;
1604 let (n, p) = validate_design(x, weights)?;
1605 let blas = CudaBlas::new(stream.clone()).map_err(|e| format!("cublas init: {e}"))?;
1606 let x_col = to_col_major(&x);
1607 let x_dev = pinned_htod(&stream, &x_col)?;
1608 let mut w_dev = pinned_htod(
1609 &stream,
1610 weights.as_slice().ok_or("weights must be contiguous")?,
1611 )?;
1612 let mut wx_dev = stream
1613 .alloc_zeros::<f64>(n.checked_mul(p).ok_or("X size overflow")?)
1614 .map_err(|e| format!("cuda alloc WX: {e}"))?;
1615 left_scale_rows(&blas, &stream, n, p, &x_dev, &mut w_dev, &mut wx_dev)?;
1616 let mut h_dev = stream
1617 .alloc_zeros::<f64>(p.checked_mul(p).ok_or("H size overflow")?)
1618 .map_err(|e| format!("cuda alloc H: {e}"))?;
1619 let n_i = to_i32(n)?;
1620 let p_i = to_i32(p)?;
1621 let cfg = GemmConfig::<f64> {
1622 transa: cublasOperation_t::CUBLAS_OP_T,
1623 transb: cublasOperation_t::CUBLAS_OP_N,
1624 m: p_i,
1625 n: p_i,
1626 k: n_i,
1627 alpha: 1.0,
1628 lda: n_i,
1629 ldb: n_i,
1630 beta: 0.0,
1631 ldc: p_i,
1632 };
1633 unsafe { blas.gemm(cfg, &x_dev, &wx_dev, &mut h_dev) }
1636 .map_err(|e| format!("cublas dgemm XtWX: {e}"))?;
1637 let h_col = stream
1638 .clone_dtoh(&h_dev)
1639 .map_err(|e| format!("download H: {e}"))?;
1640 from_col_major(&h_col, p, p).ok_or_else(|| "H layout conversion failed".to_string())
1641 }
1642
1643 pub(super) fn solve_step(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
1644 let (_, p) = validate_design(input.x, input.weights)?;
1650 if input.penalty_hessian.dim() != (p, p) {
1651 return Err(format!(
1652 "penalty Hessian shape {:?} does not match p={p}",
1653 input.penalty_hessian.dim()
1654 ));
1655 }
1656 if input.gradient.len() != p {
1657 return Err(format!(
1658 "gradient length {} does not match p={p}",
1659 input.gradient.len()
1660 ));
1661 }
1662 let n_rows = input.x.nrows();
1668 let zero_n = ndarray::Array1::<f64>::zeros(n_rows);
1669 let shared =
1670 PirlsGpuSharedData::upload_impl(input.x, zero_n.view(), zero_n.view(), zero_n.view())?;
1671 let mut ws = SigmaPirlsGpuWorkspace::allocate_impl(&shared)?;
1672 solve_step_on_stream(
1673 &shared,
1674 &mut ws,
1675 PirlsStepStreamInput {
1676 weights: input.weights,
1677 penalty_hessian: input.penalty_hessian,
1678 gradient: input.gradient,
1679 step_lm_lambda: input.step_lm_lambda,
1680 objective_ridge: input.objective_ridge,
1681 },
1682 )
1683 }
1684
1685 fn validate_design(
1686 x: ArrayView2<'_, f64>,
1687 weights: ArrayView1<'_, f64>,
1688 ) -> Result<(usize, usize), String> {
1689 let (n, p) = x.dim();
1690 if weights.len() != n {
1691 return Err(format!(
1692 "weights length {} does not match rows {n}",
1693 weights.len()
1694 ));
1695 }
1696 if n == 0 || p == 0 {
1697 return Err("empty design cannot be solved on CUDA".to_string());
1698 }
1699 Ok((n, p))
1700 }
1701
1702 fn left_scale_rows(
1703 blas: &CudaBlas,
1704 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1705 n: usize,
1706 p: usize,
1707 x_dev: &CudaSlice<f64>,
1708 w_dev: &mut CudaSlice<f64>,
1709 wx_dev: &mut CudaSlice<f64>,
1710 ) -> Result<(), String> {
1711 let n_i = to_i32(n)?;
1712 let p_i = to_i32(p)?;
1713 let handle = *blas.handle();
1714 let (x_ptr, _x_record) = x_dev.device_ptr(stream);
1715 let (w_ptr, _w_record) = w_dev.device_ptr(stream);
1716 let (wx_ptr, _wx_record) = wx_dev.device_ptr_mut(stream);
1717 let status = unsafe {
1720 cublasDdgmm(
1721 handle,
1722 cublasSideMode_t::CUBLAS_SIDE_LEFT,
1723 n_i,
1724 p_i,
1725 x_ptr as *const f64,
1726 n_i,
1727 w_ptr as *const f64,
1728 1,
1729 wx_ptr as *mut f64,
1730 n_i,
1731 )
1732 };
1733 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1734 Ok(())
1735 } else {
1736 Err(format!("cublasDdgmm failed with {status:?}"))
1737 }
1738 }
1739
1740 fn left_scale_rows_borrowed(
1745 blas: &CudaBlas,
1746 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1747 n: usize,
1748 p: usize,
1749 x_dev: &CudaSlice<f64>,
1750 w_dev: &CudaSlice<f64>,
1751 wx_dev: &mut CudaSlice<f64>,
1752 ) -> Result<(), String> {
1753 let n_i = to_i32(n)?;
1754 let p_i = to_i32(p)?;
1755 let handle = *blas.handle();
1756 let (x_ptr, _x_record) = x_dev.device_ptr(stream);
1757 let (w_ptr, _w_record) = w_dev.device_ptr(stream);
1758 let (wx_ptr, _wx_record) = wx_dev.device_ptr_mut(stream);
1759 let status = unsafe {
1764 cublasDdgmm(
1765 handle,
1766 cublasSideMode_t::CUBLAS_SIDE_LEFT,
1767 n_i,
1768 p_i,
1769 x_ptr as *const f64,
1770 n_i,
1771 w_ptr as *const f64,
1772 1,
1773 wx_ptr as *mut f64,
1774 n_i,
1775 )
1776 };
1777 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1778 Ok(())
1779 } else {
1780 Err(format!("cublasDdgmm (borrowed) failed with {status:?}"))
1781 }
1782 }
1783
1784 fn geam_add_inplace(
1792 blas: &CudaBlas,
1793 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1794 p: usize,
1795 a: &mut CudaSlice<f64>,
1796 b: &CudaSlice<f64>,
1797 ) -> Result<(), String> {
1798 let p_i = to_i32(p)?;
1799 let alpha = 1.0_f64;
1800 let beta = 1.0_f64;
1801 let handle = *blas.handle();
1802 let (b_ptr, _b_record) = b.device_ptr(stream);
1803 let (a_ptr, _a_record) = a.device_ptr_mut(stream);
1804 let out_ptr = a_ptr;
1806 let status = unsafe {
1809 cublasDgeam(
1810 handle,
1811 cublasOperation_t::CUBLAS_OP_N,
1812 cublasOperation_t::CUBLAS_OP_N,
1813 p_i,
1814 p_i,
1815 &alpha,
1816 a_ptr as *const f64,
1817 p_i,
1818 &beta,
1819 b_ptr as *const f64,
1820 p_i,
1821 out_ptr as *mut f64,
1822 p_i,
1823 )
1824 };
1825 if status == cublasStatus_t::CUBLAS_STATUS_SUCCESS {
1826 Ok(())
1827 } else {
1828 Err(format!("cublasDgeam failed with {status:?}"))
1829 }
1830 }
1831
1832 fn launch_xtwx_lower(
1836 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1837 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1838 n: usize,
1839 p: usize,
1840 x_dev: &CudaSlice<f64>,
1841 w_dev: &CudaSlice<f64>,
1842 a_dev: &mut CudaSlice<f64>,
1843 ) -> Result<(), String> {
1844 let module = FUSED_XTWX_CACHE
1845 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1846 .map_err(|e| format!("fused_xtwx module: {e}"))?;
1847 let func = module
1848 .load_function("xtwx_lower")
1849 .map_err(|e| format!("load xtwx_lower: {e}"))?;
1850 let n_i = to_i32(n)?;
1851 let p_i = to_i32(p)?;
1852 let num_pairs = p * (p + 1) / 2;
1853 let num_pairs_u32 = u32::try_from(num_pairs)
1854 .map_err(|_| format!("xtwx_lower: num_pairs {num_pairs} > u32"))?;
1855 const BLOCK: u32 = 256;
1856 let grid = num_pairs_u32.div_ceil(BLOCK).max(1);
1857 let cfg = cudarc::driver::LaunchConfig {
1858 grid_dim: (grid, 1, 1),
1859 block_dim: (BLOCK, 1, 1),
1860 shared_mem_bytes: 0,
1861 };
1862 let mut builder = stream.launch_builder(&func);
1863 builder.arg(x_dev);
1864 builder.arg(w_dev);
1865 builder.arg(a_dev);
1866 builder.arg(&n_i);
1867 builder.arg(&p_i);
1868 unsafe { builder.launch(cfg) }
1871 .map_err(|e| format!("xtwx_lower launch: {e}"))
1872 .map(|_| ())
1873 }
1874
1875 fn launch_xtscore(
1878 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1879 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1880 n: usize,
1881 p: usize,
1882 x_dev: &CudaSlice<f64>,
1883 score_dev: &CudaSlice<f64>,
1884 s_dev: &mut CudaSlice<f64>,
1885 ) -> Result<(), String> {
1886 let module = FUSED_XTWX_CACHE
1887 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1888 .map_err(|e| format!("fused_xtwx module (xtscore): {e}"))?;
1889 let func = module
1890 .load_function("xtscore")
1891 .map_err(|e| format!("load xtscore: {e}"))?;
1892 let n_i = to_i32(n)?;
1893 let p_i = to_i32(p)?;
1894 let p_u32 = u32::try_from(p).map_err(|_| format!("xtscore: p {p} > u32"))?;
1895 const BLOCK: u32 = 256;
1896 let grid = p_u32.div_ceil(BLOCK).max(1);
1897 let cfg = cudarc::driver::LaunchConfig {
1898 grid_dim: (grid, 1, 1),
1899 block_dim: (BLOCK, 1, 1),
1900 shared_mem_bytes: 0,
1901 };
1902 let mut builder = stream.launch_builder(&func);
1903 builder.arg(x_dev);
1904 builder.arg(score_dev);
1905 builder.arg(s_dev);
1906 builder.arg(&n_i);
1907 builder.arg(&p_i);
1908 unsafe { builder.launch(cfg) }
1911 .map_err(|e| format!("xtscore launch: {e}"))
1912 .map(|_| ())
1913 }
1914
1915 fn launch_symmetrize_lower(
1919 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1920 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1921 p: usize,
1922 a_dev: &mut CudaSlice<f64>,
1923 ) -> Result<(), String> {
1924 if p <= 1 {
1925 return Ok(());
1926 }
1927 let module = FUSED_XTWX_CACHE
1928 .get_or_compile(ctx, "fused_xtwx", FUSED_XTWX_PTX_SOURCE)
1929 .map_err(|e| format!("fused_xtwx module (sym): {e}"))?;
1930 let func = module
1931 .load_function("symmetrize_lower")
1932 .map_err(|e| format!("load symmetrize_lower: {e}"))?;
1933 let p_i = to_i32(p)?;
1934 let num_strict = p * (p - 1) / 2;
1935 let num_strict_u32 = u32::try_from(num_strict)
1936 .map_err(|_| format!("symmetrize_lower: num_strict {num_strict} > u32"))?;
1937 const BLOCK: u32 = 256;
1938 let grid = num_strict_u32.div_ceil(BLOCK).max(1);
1939 let cfg = cudarc::driver::LaunchConfig {
1940 grid_dim: (grid, 1, 1),
1941 block_dim: (BLOCK, 1, 1),
1942 shared_mem_bytes: 0,
1943 };
1944 let mut builder = stream.launch_builder(&func);
1945 builder.arg(a_dev);
1946 builder.arg(&p_i);
1947 unsafe { builder.launch(cfg) }
1950 .map_err(|e| format!("symmetrize_lower launch: {e}"))
1951 .map(|_| ())
1952 }
1953
1954 fn cholesky_logdet_device(
1959 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1960 ctx: &std::sync::Arc<cudarc::driver::CudaContext>,
1961 p: usize,
1962 factor_dev: &CudaSlice<f64>,
1963 ) -> Result<f64, String> {
1964 let module = CHOL_LOGDET_CACHE
1965 .get_or_compile(ctx, "pirls_gpu_chol_logdet", CHOL_LOGDET_PTX_SOURCE)
1966 .map_err(|err| format!("chol_logdet module: {err}"))?;
1967 let func = module
1968 .load_function("chol_logdet_col_major")
1969 .map_err(|err| format!("chol_logdet load_function: {err}"))?;
1970 let mut out_dev = stream
1971 .alloc_zeros::<f64>(1)
1972 .map_err(|err| format!("alloc chol_logdet out: {err}"))?;
1973 let p_i = to_i32(p)?;
1974 let cfg = LaunchConfig {
1975 grid_dim: (1, 1, 1),
1976 block_dim: (1, 1, 1),
1977 shared_mem_bytes: 0,
1978 };
1979 let mut builder = stream.launch_builder(&func);
1980 builder.arg(factor_dev);
1981 builder.arg(&p_i);
1982 builder.arg(&mut out_dev);
1983 unsafe { builder.launch(cfg) }.map_err(|err| format!("chol_logdet launch: {err}"))?;
1988 let out_host = stream
1989 .clone_dtoh(&out_dev)
1990 .map_err(|err| format!("download chol_logdet: {err}"))?;
1991 Ok(out_host[0])
1992 }
1993
1994 fn penalty_with_ridge(penalty: ArrayView2<'_, f64>, ridge: f64) -> Array2<f64> {
1995 let mut out = penalty.to_owned();
1996 if ridge != 0.0 {
1997 for i in 0..out.nrows().min(out.ncols()) {
1998 out[[i, i]] += ridge;
1999 }
2000 }
2001 out
2002 }
2003
2004 fn to_i32(value: usize) -> Result<i32, String> {
2005 i32::try_from(value).map_err(|_| format!("CUDA dimension {value} exceeds i32"))
2006 }
2007
2008 const PIRLS_LOOP_PTX_SOURCE: &str = r#"
2015// __device__ annotation required by newer NVRTC JIT semantics (see
2016// gpu_kernels/pirls_row.rs common_device_prolog — the #2313 hardware sweep).
2017extern "C" {
2018 __device__ double fabs(double);
2019}
2020
2021extern "C" __global__ void axpy_n(
2022 double alpha,
2023 const double* __restrict__ x,
2024 double* __restrict__ y,
2025 int n
2026) {
2027 int i = blockIdx.x * blockDim.x + threadIdx.x;
2028 if (i >= n) return;
2029 y[i] += alpha * x[i];
2030}
2031
2032// Correct the projected score in place:
2033// rhs = Qs^T score - S beta + linear_shift.
2034// `penalty_step` stores S + lm*I for the factorization, so subtracting its
2035// product and adding lm*beta recovers the model penalty S exactly.
2036extern "C" __global__ void correct_newton_rhs(
2037 double* __restrict__ rhs,
2038 const double* __restrict__ penalty_step,
2039 const double* __restrict__ beta,
2040 const double* __restrict__ linear_shift,
2041 double lm,
2042 int p,
2043 double* __restrict__ penalty_beta
2044) {
2045 int i = blockIdx.x * blockDim.x + threadIdx.x;
2046 if (i >= p) return;
2047 double s_beta = 0.0;
2048 for (int j = 0; j < p; ++j) {
2049 s_beta += penalty_step[i + j * p] * beta[j];
2050 }
2051 penalty_beta[i] = s_beta;
2052 rhs[i] += -s_beta + lm * beta[i] + linear_shift[i];
2053}
2054
2055extern "C" __global__ void apply_penalty(
2056 const double* __restrict__ penalty_step,
2057 const double* __restrict__ vector,
2058 int p,
2059 double* __restrict__ output
2060) {
2061 int i = blockIdx.x * blockDim.x + threadIdx.x;
2062 if (i >= p) return;
2063 double value = 0.0;
2064 for (int j = 0; j < p; ++j) {
2065 value += penalty_step[i + j * p] * vector[j];
2066 }
2067 output[i] = value;
2068}
2069
2070// Select the first acceptable member of the seven-point line-search ladder
2071// without exporting beta, direction, objectives, or refusal summaries.
2072//
2073// output layout:
2074// [0] alpha, [1] accepted data deviance, [2] accepted penalized objective,
2075// [3] halving index, [4] ||direction||_inf,
2076// [5] first refusal row, [6] first refusal code, [7] all-refused flag.
2077extern "C" __global__ void select_alpha(
2078 const double* __restrict__ data_deviance,
2079 const unsigned int* __restrict__ refusal_summary,
2080 const double* __restrict__ beta,
2081 const double* __restrict__ direction,
2082 const double* __restrict__ penalty_beta_step,
2083 const double* __restrict__ penalty_direction_step,
2084 const double* __restrict__ linear_shift,
2085 const double* __restrict__ direction_linf,
2086 double previous_deviance,
2087 double previous_objective,
2088 double constant_shift,
2089 double lm,
2090 int p,
2091 double* __restrict__ output
2092) {
2093 if (blockIdx.x != 0 || threadIdx.x != 0) return;
2094 const double alphas[7] = {
2095 1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625
2096 };
2097
2098 double penalty_beta = constant_shift;
2099 double linear_coeff_half = 0.0;
2100 double direction_penalty = 0.0;
2101 for (int i = 0; i < p; ++i) {
2102 double s_beta = penalty_beta_step[i] - lm * beta[i];
2103 double s_direction = penalty_direction_step[i] - lm * direction[i];
2104 penalty_beta += beta[i] * s_beta - 2.0 * beta[i] * linear_shift[i];
2105 linear_coeff_half += direction[i] * (s_beta - linear_shift[i]);
2106 direction_penalty += direction[i] * s_direction;
2107 }
2108
2109 output[0] = 0.0;
2110 output[1] = previous_deviance;
2111 output[2] = previous_objective;
2112 output[3] = 0.0;
2113 output[4] = direction_linf[0];
2114 output[5] = 4294967295.0;
2115 output[6] = 0.0;
2116 output[7] = 1.0;
2117
2118 for (int k = 0; k < 7; ++k) {
2119 unsigned int row = refusal_summary[k];
2120 unsigned int code = refusal_summary[7 + k];
2121 if (k == 0 && row != 0xffffffffu) {
2122 output[5] = (double)row;
2123 output[6] = (double)code;
2124 }
2125 if (row == 0xffffffffu) {
2126 output[7] = 0.0;
2127 double alpha = alphas[k];
2128 double objective = data_deviance[k]
2129 + penalty_beta
2130 + 2.0 * alpha * linear_coeff_half
2131 + alpha * alpha * direction_penalty;
2132 if (isfinite(objective) && objective <= previous_objective) {
2133 output[0] = alpha;
2134 output[1] = data_deviance[k];
2135 output[2] = objective;
2136 output[3] = (double)k;
2137 return;
2138 }
2139 }
2140 }
2141}
2142
2143extern "C" __global__ void deviance_sum(
2144 const double* __restrict__ d,
2145 int n,
2146 double* __restrict__ out
2147) {
2148 __shared__ double sm[1024];
2149 int tid = threadIdx.x;
2150 int bdim = blockDim.x;
2151 double acc = 0.0;
2152 for (int i = tid; i < n; i += bdim) {
2153 acc += d[i];
2154 }
2155 sm[tid] = acc;
2156 __syncthreads();
2157 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2158 if (tid < stride) sm[tid] += sm[tid + stride];
2159 __syncthreads();
2160 }
2161 if (tid == 0) out[0] = sm[0];
2162}
2163
2164extern "C" __global__ void linf_norm(
2165 const double* __restrict__ v,
2166 int p,
2167 double* __restrict__ out
2168) {
2169 __shared__ double sm[1024];
2170 int tid = threadIdx.x;
2171 int bdim = blockDim.x;
2172 double acc = 0.0;
2173 for (int i = tid; i < p; i += bdim) {
2174 double a = fabs(v[i]);
2175 if (a > acc) acc = a;
2176 }
2177 sm[tid] = acc;
2178 __syncthreads();
2179 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2180 if (tid < stride) {
2181 double r = sm[tid + stride];
2182 if (r > sm[tid]) sm[tid] = r;
2183 }
2184 __syncthreads();
2185 }
2186 if (tid == 0) out[0] = sm[0];
2187}
2188
2189extern "C" __global__ void negate_n(
2190 double* __restrict__ v,
2191 int n
2192) {
2193 int i = blockIdx.x * blockDim.x + threadIdx.x;
2194 if (i >= n) return;
2195 v[i] = -v[i];
2196}
2197
2198// Deterministically select the smallest failing row. out[0] is UINT_MAX on
2199// success, otherwise the row index; out[1] carries that row's refusal code.
2200extern "C" __global__ void status_first(
2201 const unsigned int* __restrict__ status,
2202 int n,
2203 unsigned int* __restrict__ out
2204) {
2205 __shared__ unsigned int sm_row[1024];
2206 __shared__ unsigned int sm_code[1024];
2207 int tid = threadIdx.x;
2208 int bdim = blockDim.x;
2209 unsigned int best_row = 0xffffffffu;
2210 unsigned int best_code = 0u;
2211 for (int i = tid; i < n; i += bdim) {
2212 unsigned int code = status[i];
2213 if (code != 0u && (unsigned int)i < best_row) {
2214 best_row = (unsigned int)i;
2215 best_code = code;
2216 }
2217 }
2218 sm_row[tid] = best_row;
2219 sm_code[tid] = best_code;
2220 __syncthreads();
2221 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2222 if (tid < stride && sm_row[tid + stride] < sm_row[tid]) {
2223 sm_row[tid] = sm_row[tid + stride];
2224 sm_code[tid] = sm_code[tid + stride];
2225 }
2226 __syncthreads();
2227 }
2228 if (tid == 0) {
2229 out[0] = sm_row[0];
2230 out[1] = sm_code[0];
2231 }
2232}
2233
2234// Same deterministic reduction for the alpha-major [7*n] ladder status
2235// matrix. One block handles each alpha; outputs are row[0..7), code[7..14).
2236extern "C" __global__ void status_first_ladder(
2237 const unsigned int* __restrict__ status,
2238 int n,
2239 unsigned int* __restrict__ out
2240) {
2241 __shared__ unsigned int sm_row[1024];
2242 __shared__ unsigned int sm_code[1024];
2243 int k = blockIdx.x;
2244 int tid = threadIdx.x;
2245 int bdim = blockDim.x;
2246 unsigned int best_row = 0xffffffffu;
2247 unsigned int best_code = 0u;
2248 const unsigned int* candidate = status + ((long long)k * n);
2249 for (int i = tid; i < n; i += bdim) {
2250 unsigned int code = candidate[i];
2251 if (code != 0u && (unsigned int)i < best_row) {
2252 best_row = (unsigned int)i;
2253 best_code = code;
2254 }
2255 }
2256 sm_row[tid] = best_row;
2257 sm_code[tid] = best_code;
2258 __syncthreads();
2259 for (int stride = bdim / 2; stride > 0; stride >>= 1) {
2260 if (tid < stride && sm_row[tid + stride] < sm_row[tid]) {
2261 sm_row[tid] = sm_row[tid + stride];
2262 sm_code[tid] = sm_code[tid + stride];
2263 }
2264 __syncthreads();
2265 }
2266 if (tid == 0) {
2267 out[k] = sm_row[0];
2268 out[7 + k] = sm_code[0];
2269 }
2270}
2271"#;
2272
2273 static PIRLS_LOOP_CACHE: PtxModuleCache = PtxModuleCache::new();
2274
2275 pub struct PirlsLoopWorkspace {
2282 pub beta_dev: CudaSlice<f64>,
2283 pub linear_shift_dev: CudaSlice<f64>,
2285 pub eta_dev: CudaSlice<f64>,
2286 pub row_solve: crate::gpu_kernels::pirls_row::SolveRowBuffers,
2288 pub alpha_ladder: crate::gpu_kernels::pirls_row::AlphaLadderDevBuffers,
2290 pub row_final: crate::gpu_kernels::pirls_row::RowOutputDevBuffers,
2292 pub direction_dev: CudaSlice<f64>,
2293 pub penalty_direction_dev: CudaSlice<f64>,
2295 pub xd_dev: CudaSlice<f64>,
2296 pub scalar_dev: CudaSlice<f64>,
2297 pub alpha_selection_dev: CudaSlice<f64>,
2301 pub status_u32_dev: CudaSlice<u32>,
2304 pub n: usize,
2305 pub p: usize,
2306 }
2307
2308 impl PirlsLoopWorkspace {
2309 pub fn allocate(
2310 shared: &PirlsGpuSharedData,
2311 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
2312 ) -> Result<Self, String> {
2313 let n = shared.n;
2314 let p = shared.p;
2315 let alloc_f64 = |label: &'static str, len: usize| {
2316 stream
2317 .alloc_zeros::<f64>(len)
2318 .map_err(|e| format!("pirls loop alloc {label}: {e}"))
2319 };
2320 Ok(Self {
2321 beta_dev: alloc_f64("beta", p)?,
2322 linear_shift_dev: alloc_f64("linear shift", p)?,
2323 eta_dev: alloc_f64("eta", n)?,
2324 row_solve: crate::gpu_kernels::pirls_row::SolveRowBuffers::allocate(stream, n)
2325 .map_err(|e| format!("pirls loop alloc row_solve: {e}"))?,
2326 alpha_ladder: crate::gpu_kernels::pirls_row::AlphaLadderDevBuffers::allocate(
2327 stream, n,
2328 )
2329 .map_err(|e| format!("pirls loop alloc alpha_ladder: {e}"))?,
2330 row_final: crate::gpu_kernels::pirls_row::RowOutputDevBuffers::allocate(stream, n)
2331 .map_err(|e| format!("pirls loop alloc row_final: {e}"))?,
2332 direction_dev: alloc_f64("direction", p)?,
2333 penalty_direction_dev: alloc_f64("penalty direction", p)?,
2334 xd_dev: alloc_f64("xd", n)?,
2335 scalar_dev: alloc_f64("scalar", 1)?,
2336 alpha_selection_dev: alloc_f64("alpha selection", 8)?,
2337 status_u32_dev: stream
2338 .alloc_zeros::<u32>(14)
2339 .map_err(|e| format!("pirls loop alloc status_u32: {e}"))?,
2340 n,
2341 p,
2342 })
2343 }
2344 }
2345
2346 pub struct PirlsLoopExtra<'a> {
2370 pub likelihood: &'a gam_problem::GlmLikelihoodSpec,
2375 pub inverse_link: &'a gam_problem::InverseLink,
2378 pub y: ndarray::ArrayView1<'a, f64>,
2381 pub priorweights: ndarray::ArrayView1<'a, f64>,
2384 pub offset: ndarray::ArrayView1<'a, f64>,
2388 pub linear_constraints: Option<&'a gam_problem::LinearInequalityConstraints>,
2395 pub exported_curvature: crate::pirls::HessianCurvatureKind,
2405 pub ridge_passport: Option<gam_problem::RidgePassport>,
2412 pub firth: Option<crate::pirls::FirthDiagnostics>,
2418 pub edf: Option<f64>,
2427 }
2428
2429 #[derive(Clone, Debug)]
2430 pub struct PirlsLoopOutcome {
2431 pub beta: Array1<f64>,
2432 pub penalized_hessian: Array2<f64>,
2433 pub logdet: f64,
2434 pub deviance: f64,
2435 pub iterations: usize,
2436 pub converged: bool,
2437 pub final_eta: Array1<f64>,
2440 pub final_mu: Array1<f64>,
2443 pub final_grad_eta: Array1<f64>,
2447 pub final_w_hessian: Array1<f64>,
2451 pub final_w_solver: Array1<f64>,
2454 pub final_offset: Array1<f64>,
2458 pub beta_transformed: Array1<f64>,
2463 pub finalweights: Array1<f64>,
2467 pub solveweights: Array1<f64>,
2470 pub solve_dmu_deta: Array1<f64>,
2474 pub solve_d2mu_deta2: Array1<f64>,
2476 pub solve_d3mu_deta3: Array1<f64>,
2478 pub solve_c_array: Array1<f64>,
2482 pub solve_d_array: Array1<f64>,
2485 pub derivatives_unsupported: bool,
2489 pub status: crate::pirls::PirlsStatus,
2495 pub ridge_passport: gam_problem::RidgePassport,
2500 pub firth: crate::pirls::FirthDiagnostics,
2503 pub constraint_kkt: Option<crate::active_set::ConstraintKktDiagnostics>,
2507 pub edf: f64,
2510 pub last_deviance_change: f64,
2514 pub last_step_halving: usize,
2520 pub last_step_size: f64,
2523 pub final_lm_lambda: f64,
2529 pub min_deviance: f64,
2536 pub max_abs_eta: f64,
2541 }
2542
2543 pub(super) fn pirls_loop(
2548 shared: &PirlsGpuSharedData,
2549 ws: &mut SigmaPirlsGpuWorkspace,
2550 loop_ws: &mut PirlsLoopWorkspace,
2551 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
2552 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
2553 gamma_shape: f64,
2556 beta0_host: ArrayView1<'_, f64>,
2557 penalty_hessian: ArrayView2<'_, f64>,
2558 linear_shift: ArrayView1<'_, f64>,
2563 constant_shift: f64,
2566 lm_ridge: f64,
2569 objective_ridge: f64,
2572 max_iter: usize,
2573 tol: f64,
2574 extra: Option<&PirlsLoopExtra<'_>>,
2575 ) -> Result<PirlsLoopOutcome, PirlsGpuLoopError> {
2576 let n = shared.n;
2577 let p = shared.p;
2578 if loop_ws.n != n || loop_ws.p != p {
2579 return Err(format!(
2580 "loop workspace ({}, {}) ≠ shared ({n}, {p})",
2581 loop_ws.n, loop_ws.p
2582 )
2583 .into());
2584 }
2585 if beta0_host.len() != p {
2586 return Err(format!("beta0 length {} ≠ p={p}", beta0_host.len()).into());
2587 }
2588
2589 if linear_shift.len() != p {
2590 return Err(format!("linear_shift length {} ≠ p={p}", linear_shift.len()).into());
2591 }
2592 if penalty_hessian.dim() != (p, p) {
2593 return Err(format!(
2594 "penalty_hessian shape {:?} ≠ (p={p}, p={p})",
2595 penalty_hessian.dim()
2596 )
2597 .into());
2598 }
2599
2600 ws.stream
2601 .memcpy_htod(
2602 beta0_host.as_slice().ok_or("beta0 not contiguous")?,
2603 &mut loop_ws.beta_dev,
2604 )
2605 .map_err(|e| format!("upload beta0: {e}"))?;
2606 ws.stream
2607 .memcpy_htod(
2608 linear_shift
2609 .as_slice()
2610 .ok_or("linear_shift not contiguous")?,
2611 &mut loop_ws.linear_shift_dev,
2612 )
2613 .map_err(|e| format!("upload linear_shift: {e}"))?;
2614
2615 let backend = crate::gpu_kernels::pirls_row::PirlsRowBackend::probe()
2616 .map_err(|e| format!("pirls_row backend: {e}"))?;
2617 let loop_module = PIRLS_LOOP_CACHE
2618 .get_or_compile(&shared.ctx, "pirls_loop", PIRLS_LOOP_PTX_SOURCE)
2619 .map_err(|e| format!("pirls loop module: {e}"))?;
2620 let axpy_func = loop_module
2621 .load_function("axpy_n")
2622 .map_err(|e| format!("load axpy_n: {e}"))?;
2623 let sum_func = loop_module
2624 .load_function("deviance_sum")
2625 .map_err(|e| format!("load deviance_sum: {e}"))?;
2626 let linf_func = loop_module
2627 .load_function("linf_norm")
2628 .map_err(|e| format!("load linf_norm: {e}"))?;
2629 let status_first_func = loop_module
2630 .load_function("status_first")
2631 .map_err(|e| format!("load status_first: {e}"))?;
2632 let status_first_ladder_func = loop_module
2633 .load_function("status_first_ladder")
2634 .map_err(|e| format!("load status_first_ladder: {e}"))?;
2635 let select_alpha_func = loop_module
2636 .load_function("select_alpha")
2637 .map_err(|e| format!("load select_alpha: {e}"))?;
2638 let apply_penalty_func = loop_module
2639 .load_function("apply_penalty")
2640 .map_err(|e| format!("load apply_penalty: {e}"))?;
2641
2642 gemv_no_trans(
2645 &ws.blas,
2646 p,
2647 p,
2648 &ws.qs_dev,
2649 &loop_ws.beta_dev,
2650 &mut ws.beta_orig_dev,
2651 )?;
2652 gemv_no_trans(
2654 &ws.blas,
2655 n,
2656 p,
2657 &shared.x_original_dev,
2658 &ws.beta_orig_dev,
2659 &mut loop_ws.eta_dev,
2660 )?;
2661 axpy(
2662 &ws.stream,
2663 &axpy_func,
2664 1.0,
2665 &shared.offset_dev,
2666 &mut loop_ws.eta_dev,
2667 n,
2668 )?;
2669 crate::gpu_kernels::pirls_row::launch_solve_row_on_stream(
2671 backend,
2672 family,
2673 curvature,
2674 gamma_shape,
2675 &ws.stream,
2676 n,
2677 &loop_ws.eta_dev,
2678 &shared.y_dev,
2679 &shared.prior_w_dev,
2680 &mut loop_ws.row_solve,
2681 )
2682 .map_err(|e| format!("solve-row init: {e}"))?;
2683 certify_device_rows(
2684 &ws.stream,
2685 &status_first_func,
2686 &loop_ws.row_solve.status,
2687 &mut loop_ws.status_u32_dev,
2688 family,
2689 curvature,
2690 gamma_shape,
2691 &loop_ws.eta_dev,
2692 &shared.y_dev,
2693 &shared.prior_w_dev,
2694 n,
2695 "solve-row init",
2696 )?;
2697
2698 let mut prev_deviance = reduce_scalar(
2699 &ws.stream,
2700 &sum_func,
2701 &loop_ws.row_solve.deviance,
2702 n,
2703 &mut loop_ws.scalar_dev,
2704 "deviance_init",
2705 )?;
2706 let mut last_logdet = 0.0_f64;
2707 let mut converged = false;
2708
2709 let s_beta0 = penalty_hessian.dot(&beta0_host);
2714 let penalty_init =
2715 beta0_host.dot(&s_beta0) - 2.0 * beta0_host.dot(&linear_shift) + constant_shift;
2716 let mut prev_objective = prev_deviance + penalty_init;
2717
2718 let mut last_dev_delta = 0.0_f64;
2726 let mut last_halving: usize = 0;
2727 let mut last_step_size = 0.0_f64;
2728 let mut min_dev = prev_deviance;
2729 let mut step_search_exhausted = false;
2730
2731 for it in 0..max_iter {
2732 last_logdet = solve_step_on_stream_device_inplace(
2733 shared,
2734 ws,
2735 PirlsStepStreamDeviceInput {
2736 w_solver_dev: &loop_ws.row_solve.w_solver,
2737 grad_eta_dev: &loop_ws.row_solve.grad_eta,
2738 penalty_hessian,
2739 step_lm_lambda: lm_ridge,
2740 objective_ridge,
2741 beta_dev: &loop_ws.beta_dev,
2742 linear_shift,
2743 },
2744 )
2745 .map_err(|e| format!("inner step it={it}: {e}"))?;
2746 ws.stream
2749 .memcpy_dtod(&ws.rhs_dev, &mut loop_ws.direction_dev)
2750 .map_err(|e| format!("direction d2d copy it={it}: {e}"))?;
2751
2752 launch_scalar_reduction(
2753 &ws.stream,
2754 &linf_func,
2755 &loop_ws.direction_dev,
2756 p,
2757 &mut loop_ws.scalar_dev,
2758 "dir_linf",
2759 )?;
2760
2761 gemv_no_trans(
2763 &ws.blas,
2764 p,
2765 p,
2766 &ws.qs_dev,
2767 &loop_ws.direction_dev,
2768 &mut ws.dir_orig_dev,
2769 )?;
2770 gemv_no_trans(
2771 &ws.blas,
2772 n,
2773 p,
2774 &shared.x_original_dev,
2775 &ws.dir_orig_dev,
2776 &mut loop_ws.xd_dev,
2777 )?;
2778
2779 loop_ws
2788 .alpha_ladder
2789 .zero(&ws.stream)
2790 .map_err(|e| format!("ladder zero it={it}: {e}"))?;
2791 crate::gpu_kernels::pirls_row::launch_alpha_ladder_on_stream(
2792 backend,
2793 family,
2794 curvature,
2795 gamma_shape,
2796 &ws.stream,
2797 n,
2798 &loop_ws.eta_dev,
2799 &loop_ws.xd_dev,
2800 &shared.y_dev,
2801 &shared.prior_w_dev,
2802 &mut loop_ws.alpha_ladder,
2803 )
2804 .map_err(|e| format!("alpha-ladder it={it}: {e}"))?;
2805 launch_ladder_status_first_reduction(
2806 &ws.stream,
2807 &status_first_ladder_func,
2808 &loop_ws.alpha_ladder.status_dev,
2809 n,
2810 &mut loop_ws.status_u32_dev,
2811 )?;
2812 let p_i = to_i32(p)?;
2813 let contraction_cfg = LaunchConfig {
2814 grid_dim: ((p as u32).div_ceil(256).max(1), 1, 1),
2815 block_dim: (256, 1, 1),
2816 shared_mem_bytes: 0,
2817 };
2818 let mut contraction_builder = ws.stream.launch_builder(&apply_penalty_func);
2819 contraction_builder.arg(&ws.penalty_dev);
2820 contraction_builder.arg(&loop_ws.direction_dev);
2821 contraction_builder.arg(&p_i);
2822 contraction_builder.arg(&mut loop_ws.penalty_direction_dev);
2823 unsafe { contraction_builder.launch(contraction_cfg) }
2826 .map_err(|e| format!("apply penalty to direction it={it}: {e}"))?;
2827 let selection_cfg = LaunchConfig {
2828 grid_dim: (1, 1, 1),
2829 block_dim: (1, 1, 1),
2830 shared_mem_bytes: 0,
2831 };
2832 let mut builder = ws.stream.launch_builder(&select_alpha_func);
2833 builder.arg(&loop_ws.alpha_ladder.objective_dev);
2834 builder.arg(&loop_ws.status_u32_dev);
2835 builder.arg(&loop_ws.beta_dev);
2836 builder.arg(&loop_ws.direction_dev);
2837 builder.arg(&ws.beta_orig_dev);
2838 builder.arg(&loop_ws.penalty_direction_dev);
2839 builder.arg(&loop_ws.linear_shift_dev);
2840 builder.arg(&loop_ws.scalar_dev);
2841 builder.arg(&prev_deviance);
2842 builder.arg(&prev_objective);
2843 builder.arg(&constant_shift);
2844 builder.arg(&lm_ridge);
2845 builder.arg(&p_i);
2846 builder.arg(&mut loop_ws.alpha_selection_dev);
2847 unsafe { builder.launch(selection_cfg) }
2851 .map_err(|e| format!("select alpha on device it={it}: {e}"))?;
2852 let selection = ws
2853 .stream
2854 .clone_dtoh(&loop_ws.alpha_selection_dev)
2855 .map_err(|e| format!("download alpha selection it={it}: {e}"))?;
2856 let alpha = selection[0];
2857 let accepted_dev = selection[1];
2858 let accepted_objective = selection[2];
2859 let halving_count = selection[3] as usize;
2860 let dir_linf = selection[4];
2861 let all_candidates_refused = selection[7] != 0.0;
2862 if alpha == 0.0 {
2863 if all_candidates_refused {
2864 let row = selection[5] as usize;
2865 let code = selection[6] as u32;
2866 let eta_host = ws
2867 .stream
2868 .clone_dtoh(&loop_ws.eta_dev)
2869 .map_err(|error| format!("ladder refusal eta download: {error}"))?;
2870 let xd_host = ws
2871 .stream
2872 .clone_dtoh(&loop_ws.xd_dev)
2873 .map_err(|error| format!("ladder refusal direction download: {error}"))?;
2874 let y_host = ws
2875 .stream
2876 .clone_dtoh(&shared.y_dev)
2877 .map_err(|error| format!("ladder refusal response download: {error}"))?;
2878 let prior_host =
2879 ws.stream.clone_dtoh(&shared.prior_w_dev).map_err(|error| {
2880 format!("ladder refusal prior-weight download: {error}")
2881 })?;
2882 let trial_eta = eta_host[row]
2883 + crate::gpu_kernels::pirls_row::ALPHA_LADDER[0] * xd_host[row];
2884 return Err(replay_row_refusal(
2885 family,
2886 curvature,
2887 gamma_shape,
2888 row,
2889 code,
2890 trial_eta,
2891 y_host[row],
2892 prior_host[row],
2893 ));
2894 }
2895 step_search_exhausted = true;
2914 last_halving = 0;
2915 last_step_size = 0.0;
2916 last_dev_delta = 0.0;
2917 break;
2918 }
2919 step_search_exhausted = false;
2920 axpy(
2922 &ws.stream,
2923 &axpy_func,
2924 alpha,
2925 &loop_ws.direction_dev,
2926 &mut loop_ws.beta_dev,
2927 p,
2928 )?;
2929 axpy(
2930 &ws.stream,
2931 &axpy_func,
2932 alpha,
2933 &loop_ws.xd_dev,
2934 &mut loop_ws.eta_dev,
2935 n,
2936 )?;
2937 crate::gpu_kernels::pirls_row::launch_solve_row_on_stream(
2939 backend,
2940 family,
2941 curvature,
2942 gamma_shape,
2943 &ws.stream,
2944 n,
2945 &loop_ws.eta_dev,
2946 &shared.y_dev,
2947 &shared.prior_w_dev,
2948 &mut loop_ws.row_solve,
2949 )
2950 .map_err(|e| format!("solve-row accepted it={it}: {e}"))?;
2951 certify_device_rows(
2952 &ws.stream,
2953 &status_first_func,
2954 &loop_ws.row_solve.status,
2955 &mut loop_ws.status_u32_dev,
2956 family,
2957 curvature,
2958 gamma_shape,
2959 &loop_ws.eta_dev,
2960 &shared.y_dev,
2961 &shared.prior_w_dev,
2962 n,
2963 "solve-row accepted",
2964 )?;
2965
2966 let step_norm = alpha.abs() * dir_linf;
2967 let dev_delta = (prev_objective - accepted_objective).abs();
2968 last_dev_delta = dev_delta;
2969 last_halving = halving_count;
2970 last_step_size = alpha;
2971 if accepted_dev < min_dev {
2972 min_dev = accepted_dev;
2973 }
2974
2975 prev_deviance = accepted_dev;
2976 prev_objective = accepted_objective;
2977
2978 if dir_linf <= tol
2979 && step_norm <= tol
2980 && dev_delta <= tol * (1.0 + prev_objective.abs())
2981 {
2982 converged = true;
2983 crate::gpu_kernels::pirls_row::launch_row_reweight_on_stream(
2985 backend,
2986 family,
2987 curvature,
2988 gamma_shape,
2989 &ws.stream,
2990 n,
2991 &loop_ws.eta_dev,
2992 &shared.y_dev,
2993 &shared.prior_w_dev,
2994 &mut loop_ws.row_final,
2995 )
2996 .map_err(|e| format!("final-row converged: {e}"))?;
2997 certify_device_rows(
2998 &ws.stream,
2999 &status_first_func,
3000 &loop_ws.row_final.status,
3001 &mut loop_ws.status_u32_dev,
3002 family,
3003 curvature,
3004 gamma_shape,
3005 &loop_ws.eta_dev,
3006 &shared.y_dev,
3007 &shared.prior_w_dev,
3008 n,
3009 "final-row converged",
3010 )?;
3011 let h_final = rebuild_h_final(
3012 shared,
3013 ws,
3014 &loop_ws.row_final.w_hessian,
3015 penalty_hessian,
3016 objective_ridge,
3017 )
3018 .map_err(|e| format!("rebuild H_final (converged): {e}"))?;
3019 return build_loop_outcome(
3020 ws,
3021 loop_ws,
3022 h_final,
3023 last_logdet,
3024 prev_deviance,
3025 it + 1,
3026 converged,
3027 lm_ridge,
3028 objective_ridge,
3029 extra,
3030 LoopDiagnostics {
3031 last_deviance_change: last_dev_delta,
3032 last_step_halving: last_halving,
3033 last_step_size,
3034 min_deviance: min_dev,
3035 step_search_exhausted,
3036 },
3037 );
3038 }
3039 }
3040
3041 crate::gpu_kernels::pirls_row::launch_row_reweight_on_stream(
3043 backend,
3044 family,
3045 curvature,
3046 gamma_shape,
3047 &ws.stream,
3048 n,
3049 &loop_ws.eta_dev,
3050 &shared.y_dev,
3051 &shared.prior_w_dev,
3052 &mut loop_ws.row_final,
3053 )
3054 .map_err(|e| format!("final-row max_iter: {e}"))?;
3055 certify_device_rows(
3056 &ws.stream,
3057 &status_first_func,
3058 &loop_ws.row_final.status,
3059 &mut loop_ws.status_u32_dev,
3060 family,
3061 curvature,
3062 gamma_shape,
3063 &loop_ws.eta_dev,
3064 &shared.y_dev,
3065 &shared.prior_w_dev,
3066 n,
3067 "final-row max_iter",
3068 )?;
3069 let h_final = rebuild_h_final(
3070 shared,
3071 ws,
3072 &loop_ws.row_final.w_hessian,
3073 penalty_hessian,
3074 objective_ridge,
3075 )
3076 .map_err(|e| format!("rebuild H_final (max_iter): {e}"))?;
3077 build_loop_outcome(
3078 ws,
3079 loop_ws,
3080 h_final,
3081 last_logdet,
3082 prev_deviance,
3083 max_iter,
3084 converged,
3085 lm_ridge,
3086 objective_ridge,
3087 extra,
3088 LoopDiagnostics {
3089 last_deviance_change: last_dev_delta,
3090 last_step_halving: last_halving,
3091 last_step_size,
3092 min_deviance: min_dev,
3093 step_search_exhausted,
3094 },
3095 )
3096 }
3097
3098 struct LoopDiagnostics {
3111 last_deviance_change: f64,
3112 last_step_halving: usize,
3113 last_step_size: f64,
3114 min_deviance: f64,
3115 step_search_exhausted: bool,
3116 }
3117
3118 fn build_loop_outcome(
3133 ws: &mut SigmaPirlsGpuWorkspace,
3134 loop_ws: &mut PirlsLoopWorkspace,
3135 penalized_hessian: Array2<f64>,
3136 logdet: f64,
3137 deviance: f64,
3138 iterations: usize,
3139 converged: bool,
3140 step_lm_lambda: f64,
3141 objective_ridge: f64,
3142 extra: Option<&PirlsLoopExtra<'_>>,
3143 diagnostics: LoopDiagnostics,
3144 ) -> Result<PirlsLoopOutcome, PirlsGpuLoopError> {
3145 let beta = download_vec(&ws.stream, &loop_ws.beta_dev)?;
3146 let final_eta = download_vec(&ws.stream, &loop_ws.eta_dev)?;
3147 let final_mu = download_vec(&ws.stream, &loop_ws.row_final.mu)?;
3148 let final_grad_eta = download_vec(&ws.stream, &loop_ws.row_final.grad_eta)?;
3149 let final_w_hessian = download_vec(&ws.stream, &loop_ws.row_final.w_hessian)?;
3150 let final_w_solver = download_vec(&ws.stream, &loop_ws.row_final.w_solver)?;
3151
3152 let eta_finite = final_eta.iter().all(|v| v.is_finite());
3157 let mu_finite = final_mu.iter().all(|v| v.is_finite());
3158 let beta_finite = beta.iter().all(|v| v.is_finite());
3159 let stability_ok = eta_finite && mu_finite && beta_finite;
3160 let status = if !stability_ok {
3161 crate::pirls::PirlsStatus::Unstable
3162 } else if converged {
3163 crate::pirls::PirlsStatus::Converged
3164 } else if diagnostics.step_search_exhausted {
3165 crate::pirls::PirlsStatus::LmStepSearchExhausted
3173 } else {
3174 crate::pirls::PirlsStatus::MaxIterationsReached
3175 };
3176
3177 let default_ridge = gam_problem::RidgePassport::scaled_identity(
3180 objective_ridge,
3181 gam_linalg::RidgePolicy::exact_full_objective(),
3182 )
3183 .map_err(gam_problem::EstimationError::from)?;
3184
3185 let max_abs_eta = final_eta.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
3186
3187 match extra {
3188 Some(ext) => {
3189 let (score_c, score_d, solve_dmu_deta, solve_d2mu_deta2, solve_d3mu_deta3) =
3192 crate::pirls::computeworkingweight_derivatives_from_eta(
3193 ext.likelihood,
3194 ext.inverse_link,
3195 &final_eta,
3196 ext.priorweights,
3197 )
3198 .map_err(PirlsGpuLoopError::Geometry)?;
3199
3200 let (finalweights, solve_c_array, solve_d_array) = match ext.exported_curvature {
3201 crate::pirls::HessianCurvatureKind::Observed => {
3202 crate::pirls::compute_observed_hessian_curvature_arrays(
3203 ext.likelihood,
3204 ext.inverse_link,
3205 &final_eta,
3206 ext.y,
3207 &final_w_solver,
3208 ext.priorweights,
3209 )
3210 .map_err(PirlsGpuLoopError::Geometry)?
3211 }
3212 crate::pirls::HessianCurvatureKind::Fisher => {
3213 (final_w_solver.clone(), score_c.clone(), score_d.clone())
3214 }
3215 };
3216
3217 let beta_transformed = beta.clone();
3223
3224 let constraint_kkt = ext.linear_constraints.and_then(|lin| {
3225 if lin.a.nrows() == 0 {
3226 return None;
3227 }
3228 let grad = penalized_hessian.dot(&beta);
3234 Some(crate::active_set::compute_constraint_kkt_diagnostics(
3235 &beta, &grad, lin,
3236 ))
3237 });
3238
3239 let ridge_passport = ext.ridge_passport.unwrap_or(default_ridge);
3240 let firth = ext
3241 .firth
3242 .clone()
3243 .unwrap_or(crate::pirls::FirthDiagnostics::Inactive);
3244 let edf = ext.edf.unwrap_or(f64::NAN);
3245 let derivatives_unsupported = false;
3251
3252 Ok(PirlsLoopOutcome {
3253 beta,
3254 penalized_hessian,
3255 logdet,
3256 deviance,
3257 iterations,
3258 converged,
3259 final_eta,
3260 final_mu,
3261 final_grad_eta,
3262 final_w_hessian,
3263 final_w_solver: final_w_solver.clone(),
3264 final_offset: ext.offset.to_owned(),
3265 beta_transformed,
3266 finalweights,
3267 solveweights: final_w_solver,
3268 solve_dmu_deta,
3269 solve_d2mu_deta2,
3270 solve_d3mu_deta3,
3271 solve_c_array,
3272 solve_d_array,
3273 derivatives_unsupported,
3274 status,
3275 ridge_passport,
3276 firth,
3277 constraint_kkt,
3278 edf,
3279 last_deviance_change: diagnostics.last_deviance_change,
3280 last_step_halving: diagnostics.last_step_halving,
3281 last_step_size: diagnostics.last_step_size,
3282 final_lm_lambda: step_lm_lambda,
3283 min_deviance: diagnostics.min_deviance,
3284 max_abs_eta,
3285 })
3286 }
3287 None => {
3288 Ok(PirlsLoopOutcome {
3296 beta: beta.clone(),
3297 penalized_hessian,
3298 logdet,
3299 deviance,
3300 iterations,
3301 converged,
3302 final_eta,
3303 final_mu,
3304 final_grad_eta,
3305 final_w_hessian,
3306 final_w_solver: final_w_solver.clone(),
3307 final_offset: Array1::<f64>::zeros(0),
3308 beta_transformed: beta,
3309 finalweights: Array1::<f64>::zeros(0),
3310 solveweights: final_w_solver,
3311 solve_dmu_deta: Array1::<f64>::zeros(0),
3312 solve_d2mu_deta2: Array1::<f64>::zeros(0),
3313 solve_d3mu_deta3: Array1::<f64>::zeros(0),
3314 solve_c_array: Array1::<f64>::zeros(0),
3315 solve_d_array: Array1::<f64>::zeros(0),
3316 derivatives_unsupported: true,
3317 status,
3318 ridge_passport: default_ridge,
3319 firth: crate::pirls::FirthDiagnostics::Inactive,
3320 constraint_kkt: None,
3321 edf: f64::NAN,
3322 last_deviance_change: diagnostics.last_deviance_change,
3323 last_step_halving: diagnostics.last_step_halving,
3324 last_step_size: diagnostics.last_step_size,
3325 final_lm_lambda: step_lm_lambda,
3326 min_deviance: diagnostics.min_deviance,
3327 max_abs_eta,
3328 })
3329 }
3330 }
3331 }
3332
3333 fn gemv_no_trans(
3334 blas: &CudaBlas,
3335 n: usize,
3336 p: usize,
3337 a_dev: &CudaSlice<f64>,
3338 x_dev: &CudaSlice<f64>,
3339 y_dev: &mut CudaSlice<f64>,
3340 ) -> Result<(), String> {
3341 let n_i = to_i32(n)?;
3342 let p_i = to_i32(p)?;
3343 let cfg = GemvConfig::<f64> {
3344 trans: cublasOperation_t::CUBLAS_OP_N,
3345 m: n_i,
3346 n: p_i,
3347 alpha: 1.0,
3348 lda: n_i,
3349 incx: 1,
3350 beta: 0.0,
3351 incy: 1,
3352 };
3353 unsafe { blas.gemv(cfg, a_dev, x_dev, y_dev) }.map_err(|e| format!("dgemv no-trans: {e}"))
3355 }
3356
3357 fn axpy(
3358 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3359 func: &cudarc::driver::CudaFunction,
3360 alpha: f64,
3361 x_dev: &CudaSlice<f64>,
3362 y_dev: &mut CudaSlice<f64>,
3363 n: usize,
3364 ) -> Result<(), String> {
3365 const THREADS: u32 = 256;
3366 let n_i = to_i32(n)?;
3367 let n_u = u32::try_from(n).map_err(|_| format!("axpy n={n} > u32"))?;
3368 let grid = n_u.div_ceil(THREADS).max(1);
3369 let cfg = LaunchConfig {
3370 grid_dim: (grid, 1, 1),
3371 block_dim: (THREADS, 1, 1),
3372 shared_mem_bytes: 0,
3373 };
3374 let mut builder = stream.launch_builder(func);
3375 builder.arg(&alpha);
3376 builder.arg(x_dev);
3377 builder.arg(y_dev);
3378 builder.arg(&n_i);
3379 unsafe { builder.launch(cfg) }
3382 .map(|_event_pair| ())
3383 .map_err(|e| format!("axpy launch: {e}"))
3384 }
3385
3386 fn launch_scalar_reduction(
3387 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3388 func: &cudarc::driver::CudaFunction,
3389 src: &CudaSlice<f64>,
3390 len: usize,
3391 scalar_dev: &mut CudaSlice<f64>,
3392 label: &'static str,
3393 ) -> Result<(), String> {
3394 const THREADS: u32 = 1024;
3395 let len_i = to_i32(len)?;
3396 let cfg = LaunchConfig {
3397 grid_dim: (1, 1, 1),
3398 block_dim: (THREADS, 1, 1),
3399 shared_mem_bytes: 0,
3400 };
3401 let mut builder = stream.launch_builder(func);
3402 builder.arg(src);
3403 builder.arg(&len_i);
3404 builder.arg(&mut *scalar_dev);
3405 unsafe { builder.launch(cfg) }.map_err(|e| format!("{label} reduce launch: {e}"))?;
3409 Ok(())
3410 }
3411
3412 fn reduce_scalar(
3413 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3414 func: &cudarc::driver::CudaFunction,
3415 src: &CudaSlice<f64>,
3416 len: usize,
3417 scalar_dev: &mut CudaSlice<f64>,
3418 label: &'static str,
3419 ) -> Result<f64, String> {
3420 launch_scalar_reduction(stream, func, src, len, scalar_dev, label)?;
3421 let host = stream
3422 .clone_dtoh(scalar_dev)
3423 .map_err(|e| format!("download {label}: {e}"))?;
3424 Ok(host[0])
3425 }
3426
3427 fn reduce_status_first(
3430 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3431 func: &cudarc::driver::CudaFunction,
3432 src: &CudaSlice<u32>,
3433 len: usize,
3434 status_dev: &mut CudaSlice<u32>,
3435 label: &'static str,
3436 ) -> Result<Option<(usize, u32)>, String> {
3437 const THREADS: u32 = 1024;
3438 let len_i = to_i32(len)?;
3439 let cfg = LaunchConfig {
3440 grid_dim: (1, 1, 1),
3441 block_dim: (THREADS, 1, 1),
3442 shared_mem_bytes: 0,
3443 };
3444 let mut builder = stream.launch_builder(func);
3445 builder.arg(src);
3446 builder.arg(&len_i);
3447 builder.arg(&mut *status_dev);
3448 unsafe { builder.launch(cfg) }.map_err(|e| format!("{label} first reduce launch: {e}"))?;
3451 let host = stream
3452 .clone_dtoh(status_dev)
3453 .map_err(|e| format!("download {label}: {e}"))?;
3454 if host[0] == u32::MAX {
3455 Ok(None)
3456 } else {
3457 Ok(Some((host[0] as usize, host[1])))
3458 }
3459 }
3460
3461 fn launch_ladder_status_first_reduction(
3464 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3465 func: &cudarc::driver::CudaFunction,
3466 src: &CudaSlice<u32>,
3467 n: usize,
3468 status_dev: &mut CudaSlice<u32>,
3469 ) -> Result<(), String> {
3470 const THREADS: u32 = 1024;
3471 let n_i = to_i32(n)?;
3472 let cfg = LaunchConfig {
3473 grid_dim: (crate::gpu_kernels::pirls_row::ALPHA_LADDER_LEN as u32, 1, 1),
3474 block_dim: (THREADS, 1, 1),
3475 shared_mem_bytes: 0,
3476 };
3477 let mut builder = stream.launch_builder(func);
3478 builder.arg(src);
3479 builder.arg(&n_i);
3480 builder.arg(&mut *status_dev);
3481 unsafe { builder.launch(cfg) }
3484 .map_err(|e| format!("alpha-ladder status reduction launch: {e}"))?;
3485 Ok(())
3486 }
3487
3488 fn replay_row_refusal(
3489 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3490 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3491 gamma_shape: f64,
3492 row: usize,
3493 code: u32,
3494 eta: f64,
3495 y: f64,
3496 prior_weight: f64,
3497 ) -> PirlsGpuLoopError {
3498 let input = crate::gpu_kernels::pirls_row::RowInput {
3499 eta,
3500 y,
3501 prior_weight,
3502 };
3503 match crate::gpu_kernels::pirls_row::row_reweight_cpu_at(
3504 row,
3505 family,
3506 curvature,
3507 input,
3508 gamma_shape,
3509 ) {
3510 Err(error) => PirlsGpuLoopError::Geometry(error),
3511 Ok(_) => PirlsGpuLoopError::Geometry(
3512 gam_problem::EstimationError::PirlsRowGeometryUnrepresentable {
3513 row,
3514 quantity: crate::gpu_kernels::pirls_row::status_codes::quantity(code),
3515 eta,
3516 value: f64::from(code),
3517 },
3518 ),
3519 }
3520 }
3521
3522 fn certify_device_rows(
3523 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3524 status_first_func: &cudarc::driver::CudaFunction,
3525 status: &CudaSlice<u32>,
3526 status_scratch: &mut CudaSlice<u32>,
3527 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3528 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3529 gamma_shape: f64,
3530 eta: &CudaSlice<f64>,
3531 y: &CudaSlice<f64>,
3532 prior_weight: &CudaSlice<f64>,
3533 n: usize,
3534 label: &'static str,
3535 ) -> Result<(), PirlsGpuLoopError> {
3536 let Some((_row, _code)) =
3537 reduce_status_first(stream, status_first_func, status, n, status_scratch, label)?
3538 else {
3539 return Ok(());
3540 };
3541 let eta_host = stream
3542 .clone_dtoh(eta)
3543 .map_err(|error| format!("{label} refusal eta download: {error}"))?;
3544 let y_host = stream
3545 .clone_dtoh(y)
3546 .map_err(|error| format!("{label} refusal response download: {error}"))?;
3547 let prior_host = stream
3548 .clone_dtoh(prior_weight)
3549 .map_err(|error| format!("{label} refusal prior-weight download: {error}"))?;
3550 let status_host = stream
3551 .clone_dtoh(status)
3552 .map_err(|error| format!("{label} refusal status download: {error}"))?;
3553 crate::gpu_kernels::pirls_row::replay_first_refusal(
3554 family,
3555 curvature,
3556 gamma_shape,
3557 &eta_host,
3558 &y_host,
3559 &prior_host,
3560 &status_host,
3561 )
3562 .map_err(PirlsGpuLoopError::Geometry)
3563 }
3564
3565 fn download_vec(
3566 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
3567 dev: &CudaSlice<f64>,
3568 ) -> Result<Array1<f64>, String> {
3569 let host = stream
3570 .clone_dtoh(dev)
3571 .map_err(|e| format!("download vec: {e}"))?;
3572 Ok(Array1::from_vec(host))
3573 }
3574
3575 pub struct GaussianPlsResult {
3577 pub beta: Array1<f64>,
3578 pub penalized_hessian: Array2<f64>,
3579 pub logdet: f64,
3580 }
3581
3582 pub fn solve_gaussian_pls_on_stream(
3585 a_orig: ArrayView2<'_, f64>,
3586 b_orig: ArrayView1<'_, f64>,
3587 s_transformed: ArrayView2<'_, f64>,
3588 linear_shift: ArrayView1<'_, f64>,
3589 prior_mean_target: ArrayView1<'_, f64>,
3590 ridge: f64,
3591 qs: Option<ArrayView2<'_, f64>>,
3592 ) -> Result<GaussianPlsResult, String> {
3593 let p = b_orig.len();
3594 if a_orig.dim() != (p, p) {
3595 return Err(format!("A shape {:?} != ({p},{p})", a_orig.dim()));
3596 }
3597 if s_transformed.dim() != (p, p) {
3598 return Err(format!("S shape {:?} != ({p},{p})", s_transformed.dim()));
3599 }
3600 if linear_shift.len() != p {
3601 return Err(format!("linear_shift len {} != p={p}", linear_shift.len()));
3602 }
3603 if prior_mean_target.len() != p {
3604 return Err(format!(
3605 "prior_mean_target len {} != p={p}",
3606 prior_mean_target.len()
3607 ));
3608 }
3609 if let Some(qs_v) = qs {
3610 if qs_v.dim() != (p, p) {
3611 return Err(format!("qs shape {:?} != ({p},{p})", qs_v.dim()));
3612 }
3613 }
3614 let (h_rotated, rhs_base) = if let Some(qs_v) = qs {
3615 let qs_owned = qs_v.to_owned();
3616 let tmp = a_orig.dot(&qs_owned);
3617 let h = qs_owned.t().dot(&tmp);
3618 let rb = qs_owned.t().dot(&b_orig);
3619 (h, rb)
3620 } else {
3621 (a_orig.to_owned(), b_orig.to_owned())
3622 };
3623 let penalized_hessian: Array2<f64> = &h_rotated + &s_transformed;
3624 let mut regularized = penalized_hessian.clone();
3625 if ridge > 0.0 {
3626 for i in 0..p {
3627 regularized[[i, i]] += ridge;
3628 }
3629 }
3630 let mut rhs_host = rhs_base;
3631 rhs_host += &linear_shift;
3632 if ridge > 0.0 {
3633 rhs_host.scaled_add(ridge, &prior_mean_target);
3634 }
3635 let (ctx, stream) = context_and_stream()?;
3636 let solver = DnHandle::new(stream.clone())
3637 .map_err(|e| format!("cusolver init (gaussian pls): {e}"))?;
3638 let pp = p.checked_mul(p).ok_or("p*p overflow (gaussian pls)")?;
3639 let mut h_dev = stream
3640 .alloc_zeros::<f64>(pp)
3641 .map_err(|e| format!("alloc H (gaussian pls): {e}"))?;
3642 let mut rhs_dev = stream
3643 .alloc_zeros::<f64>(p)
3644 .map_err(|e| format!("alloc rhs (gaussian pls): {e}"))?;
3645 let potrf_lwork_usize = potrf_query_lwork(&solver, &stream, p)?;
3646 let potrf_lwork = i32::try_from(potrf_lwork_usize)
3647 .map_err(|_| "potrf lwork overflow (gaussian pls)".to_string())?;
3648 let mut potrf_work_dev = stream
3649 .alloc_zeros::<f64>(potrf_lwork_usize.max(1))
3650 .map_err(|e| format!("alloc potrf workspace (gaussian pls): {e}"))?;
3651 let mut potrf_info_dev = stream
3652 .alloc_zeros::<i32>(1)
3653 .map_err(|e| format!("alloc potrf info (gaussian pls): {e}"))?;
3654 let mut potrs_info_dev = stream
3655 .alloc_zeros::<i32>(1)
3656 .map_err(|e| format!("alloc potrs info (gaussian pls): {e}"))?;
3657 let reg_col = to_col_major(®ularized);
3658 stream
3659 .memcpy_htod(reg_col.as_ref(), &mut h_dev)
3660 .map_err(|e| format!("upload H (gaussian pls): {e}"))?;
3661 let rhs_slice = rhs_host
3662 .as_slice()
3663 .ok_or("rhs_host not contiguous (gaussian pls)")?;
3664 stream
3665 .memcpy_htod(rhs_slice, &mut rhs_dev)
3666 .map_err(|e| format!("upload rhs (gaussian pls): {e}"))?;
3667 potrf_in_place_reuse(
3668 &solver,
3669 &stream,
3670 p,
3671 potrf_lwork,
3672 &mut h_dev,
3673 &mut potrf_work_dev,
3674 &mut potrf_info_dev,
3675 )?;
3676 potrs_in_place_reuse(
3677 &solver,
3678 &stream,
3679 p,
3680 1,
3681 &h_dev,
3682 &mut rhs_dev,
3683 &mut potrs_info_dev,
3684 )?;
3685 let logdet = cholesky_logdet_device(&stream, &ctx, p, &h_dev)?;
3686 let beta_raw = stream
3687 .clone_dtoh(&rhs_dev)
3688 .map_err(|e| format!("download beta (gaussian pls): {e}"))?;
3689 check_deferred_potrf_info(&stream, &potrf_info_dev)?;
3690 check_deferred_potrs_info(&stream, &potrs_info_dev)?;
3691 Ok(GaussianPlsResult {
3692 beta: Array1::from_vec(beta_raw),
3693 penalized_hessian,
3694 logdet,
3695 })
3696 }
3697}
3698
3699pub fn weighted_crossprod_gpu(
3700 x: ArrayView2<'_, f64>,
3701 weights: ArrayView1<'_, f64>,
3702) -> Result<Array2<f64>, String> {
3703 #[cfg(not(target_os = "linux"))]
3704 {
3705 return cpu_fallback::weighted_crossprod_cpu(x, weights);
3706 }
3707
3708 #[cfg(target_os = "linux")]
3709 {
3710 if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
3711 .map_err(|error| error.to_string())?
3712 .is_none()
3713 {
3714 return cpu_fallback::weighted_crossprod_cpu(x, weights);
3715 }
3716 cuda::weighted_crossprod(x, weights)
3717 }
3718}
3719
3720pub fn solve_pirls_step_gpu(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
3721 #[cfg(not(target_os = "linux"))]
3722 {
3723 return cpu_fallback::solve_step_cpu(input);
3724 }
3725
3726 #[cfg(target_os = "linux")]
3727 {
3728 if gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::global_policy())
3729 .map_err(|error| error.to_string())?
3730 .is_none()
3731 {
3732 return cpu_fallback::solve_step_cpu(input);
3733 }
3734 cuda::solve_step(input)
3735 }
3736}
3737
3738#[cfg(target_os = "linux")]
3744pub fn upload_shared_pirls_gpu(
3745 x: ndarray::ArrayView2<'_, f64>,
3746 y: ndarray::ArrayView1<'_, f64>,
3747 prior_w: ndarray::ArrayView1<'_, f64>,
3748 offset: ndarray::ArrayView1<'_, f64>,
3749) -> Result<PirlsGpuSharedData, String> {
3750 gam_gpu::device_runtime::GpuRuntime::require()
3751 .map_err(|error| format!("cannot upload shared GPU PIRLS data: {error}"))?;
3752 PirlsGpuSharedData::upload_impl(x, y, prior_w, offset)
3753}
3754
3755#[cfg(target_os = "linux")]
3759pub fn allocate_sigma_pirls_workspace(
3760 shared: &PirlsGpuSharedData,
3761) -> Result<SigmaPirlsGpuWorkspace, String> {
3762 SigmaPirlsGpuWorkspace::allocate_impl(shared)
3763}
3764
3765#[cfg(target_os = "linux")]
3770pub fn upload_qs_pirls(
3771 ws: &mut SigmaPirlsGpuWorkspace,
3772 qs: ndarray::ArrayView2<'_, f64>,
3773) -> Result<(), String> {
3774 cuda::upload_qs(ws, qs)
3775}
3776
3777#[cfg(target_os = "linux")]
3780pub fn upload_qs_identity_pirls(ws: &mut SigmaPirlsGpuWorkspace) -> Result<(), String> {
3781 cuda::upload_qs_identity(ws)
3782}
3783
3784#[cfg(target_os = "linux")]
3790pub fn solve_pirls_step_on_stream(
3791 shared: &PirlsGpuSharedData,
3792 ws: &mut SigmaPirlsGpuWorkspace,
3793 input: PirlsStepStreamInput<'_>,
3794) -> Result<PirlsGpuStep, String> {
3795 cuda::solve_step_on_stream(shared, ws, input)
3796}
3797
3798#[cfg(target_os = "linux")]
3806pub fn solve_pirls_step_on_stream_device(
3807 shared: &PirlsGpuSharedData,
3808 ws: &mut SigmaPirlsGpuWorkspace,
3809 input: PirlsStepStreamDeviceInput<'_, '_>,
3810) -> Result<PirlsGpuStep, String> {
3811 cuda::solve_step_on_stream_device(shared, ws, input)
3812}
3813
3814#[cfg(target_os = "linux")]
3824pub(crate) fn pirls_loop_on_stream(
3825 shared: &PirlsGpuSharedData,
3826 ws: &mut SigmaPirlsGpuWorkspace,
3827 loop_ws: &mut cuda::PirlsLoopWorkspace,
3828 family: crate::gpu_kernels::pirls_row::PirlsRowFamily,
3829 curvature: crate::gpu_kernels::pirls_row::CurvatureMode,
3830 likelihood_scale: PirlsLoopLikelihoodScale,
3831 beta0: ndarray::ArrayView1<'_, f64>,
3832 penalty_hessian: ndarray::ArrayView2<'_, f64>,
3833 linear_shift: ndarray::ArrayView1<'_, f64>,
3836 constant_shift: f64,
3838 step_lm_lambda: f64,
3839 objective_ridge: f64,
3840 max_iter: usize,
3841 tol: f64,
3842 extra: Option<&cuda::PirlsLoopExtra<'_>>,
3843) -> Result<cuda::PirlsLoopOutcome, cuda::PirlsGpuLoopError> {
3844 let gamma_shape = likelihood_scale
3845 .kernel_argument(family)
3846 .map_err(cuda::PirlsGpuLoopError::Runtime)?;
3847 cuda::pirls_loop(
3848 shared,
3849 ws,
3850 loop_ws,
3851 family,
3852 curvature,
3853 gamma_shape,
3854 beta0,
3855 penalty_hessian,
3856 linear_shift,
3857 constant_shift,
3858 step_lm_lambda,
3859 objective_ridge,
3860 max_iter,
3861 tol,
3862 extra,
3863 )
3864}
3865
3866#[cfg(target_os = "linux")]
3869pub fn allocate_pirls_loop_workspace(
3870 shared: &PirlsGpuSharedData,
3871 ws: &SigmaPirlsGpuWorkspace,
3872) -> Result<cuda::PirlsLoopWorkspace, String> {
3873 cuda::PirlsLoopWorkspace::allocate(shared, &ws.stream)
3874}
3875
3876#[cfg(target_os = "linux")]
3882pub fn solve_gaussian_pls_gpu(
3883 a_orig: ndarray::ArrayView2<'_, f64>,
3884 b_orig: ndarray::ArrayView1<'_, f64>,
3885 s_transformed: ndarray::ArrayView2<'_, f64>,
3886 linear_shift: ndarray::ArrayView1<'_, f64>,
3887 prior_mean_target: ndarray::ArrayView1<'_, f64>,
3888 ridge: f64,
3889 qs: Option<ndarray::ArrayView2<'_, f64>>,
3890) -> Result<cuda::GaussianPlsResult, String> {
3891 cuda::solve_gaussian_pls_on_stream(
3892 a_orig,
3893 b_orig,
3894 s_transformed,
3895 linear_shift,
3896 prior_mean_target,
3897 ridge,
3898 qs,
3899 )
3900}
3901
3902mod cpu_fallback {
3910 use super::{PirlsGpuInput, PirlsGpuStep};
3911 use crate::estimate::reml::assembly::xt_diag_x_dense_into;
3912 use faer::Side;
3913 use gam_linalg::faer_ndarray::FaerCholesky;
3914 use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
3915
3916 pub(super) fn weighted_crossprod_cpu(
3917 x: ArrayView2<'_, f64>,
3918 weights: ArrayView1<'_, f64>,
3919 ) -> Result<Array2<f64>, String> {
3920 validate(x, weights)?;
3921 let x_owned = x.to_owned();
3922 let w_owned = weights.to_owned();
3923 let mut scratch = Array2::<f64>::zeros(x_owned.dim());
3924 Ok(xt_diag_x_dense_into(&x_owned, &w_owned, &mut scratch))
3925 }
3926
3927 pub(super) fn solve_step_cpu(input: PirlsGpuInput<'_>) -> Result<PirlsGpuStep, String> {
3928 validate(input.x, input.weights)?;
3929 let (_n, p) = input.x.dim();
3930 if input.penalty_hessian.dim() != (p, p) {
3931 return Err(format!(
3932 "penalty Hessian shape {:?} does not match p={p}",
3933 input.penalty_hessian.dim()
3934 ));
3935 }
3936 if input.gradient.len() != p {
3937 return Err(format!(
3938 "gradient length {} does not match p={p}",
3939 input.gradient.len()
3940 ));
3941 }
3942 let xtwx = weighted_crossprod_cpu(input.x, input.weights)?;
3943 let mut penalized_hessian = xtwx.clone();
3945 penalized_hessian += &input.penalty_hessian;
3946 if input.objective_ridge != 0.0 {
3947 for i in 0..p {
3948 penalized_hessian[[i, i]] += input.objective_ridge;
3949 }
3950 }
3951 let mut h_step = xtwx;
3953 h_step += &input.penalty_hessian;
3954 if input.step_lm_lambda != 0.0 {
3955 for i in 0..p {
3956 h_step[[i, i]] += input.step_lm_lambda;
3957 }
3958 }
3959 let factor = h_step
3960 .cholesky(Side::Lower)
3961 .map_err(|e| format!("CPU Cholesky failed in PIRLS fallback: {e:?}"))?;
3962 let g = Array1::from_iter(input.gradient.iter().copied());
3963 let direction = factor.solvevec(&g);
3966 let logdet = 2.0 * factor.diag().iter().map(|v| v.ln()).sum::<f64>();
3968 Ok(PirlsGpuStep {
3969 penalized_hessian,
3970 direction,
3971 logdet,
3972 })
3973 }
3974
3975 fn validate(x: ArrayView2<'_, f64>, weights: ArrayView1<'_, f64>) -> Result<(), String> {
3976 let (n, p) = x.dim();
3977 if weights.len() != n {
3978 return Err(format!(
3979 "weights length {} does not match rows {n}",
3980 weights.len()
3981 ));
3982 }
3983 if n == 0 || p == 0 {
3984 return Err("empty design cannot be solved".to_string());
3985 }
3986 Ok(())
3987 }
3988}
3989
3990pub fn cholesky_solve_gpu(
3991 hessian: ArrayView2<'_, f64>,
3992 rhs: ArrayView2<'_, f64>,
3993) -> Result<(Array2<f64>, f64), String> {
3994 gam_gpu::solver::cholesky_solve_gpu(hessian, rhs)
3995}
3996
3997pub fn cholesky_solve_only_gpu(
4001 hessian: ArrayView2<'_, f64>,
4002 rhs: ArrayView2<'_, f64>,
4003) -> Result<Array2<f64>, String> {
4004 gam_gpu::solver::cholesky_solve_only_gpu(hessian, rhs)
4005}
4006
4007pub fn cholesky_lower_gpu(hessian: ArrayView2<'_, f64>) -> Result<Array2<f64>, String> {
4008 gam_gpu::solver::cholesky_lower_gpu(hessian)
4009}
4010
4011#[cfg(all(test, target_os = "linux"))]
4012mod pirls_loop_likelihood_scale_tests {
4013 use super::PirlsLoopLikelihoodScale;
4014 use crate::gpu_kernels::pirls_row::PirlsRowFamily;
4015
4016 #[test]
4017 fn gpu_row_scale_discriminant_rejects_family_mismatch() {
4018 assert!(
4019 PirlsLoopLikelihoodScale::non_gamma()
4020 .kernel_argument(PirlsRowFamily::GammaLog)
4021 .is_err()
4022 );
4023 let gamma = PirlsLoopLikelihoodScale::gamma_shape(2.0).expect("positive Gamma shape");
4024 assert!(gamma.kernel_argument(PirlsRowFamily::PoissonLog).is_err());
4025 assert_eq!(
4026 gamma
4027 .kernel_argument(PirlsRowFamily::GammaLog)
4028 .expect("matching Gamma contract"),
4029 2.0
4030 );
4031 }
4032
4033 #[test]
4034 fn non_gamma_kernel_scalar_is_poisoned_not_unit_scaled() {
4035 let abi_value = PirlsLoopLikelihoodScale::non_gamma()
4036 .kernel_argument(PirlsRowFamily::PoissonLog)
4037 .expect("matching non-Gamma contract");
4038 assert!(abi_value.is_nan());
4039 }
4040}
4041
4042#[cfg(all(test, target_os = "linux"))]
4048mod stream_device_parity_tests {
4049 use super::*;
4050 use ndarray::arr2;
4051
4052 fn device_available() -> bool {
4053 gam_gpu::device_runtime::GpuRuntime::resolve(gam_gpu::GpuPolicy::Auto)
4054 .unwrap_or_else(|error| panic!("GPU probe fault in PIRLS device test: {error}"))
4055 .is_some()
4056 }
4057
4058 fn assert_device_seam_declines_without_cuda() {
4067 let x = arr2(&[[1.0, 0.0], [0.0, 1.0]]);
4068 let y = ndarray::Array1::<f64>::zeros(2);
4069 let prior_w = ndarray::Array1::<f64>::ones(2);
4070 let offset = ndarray::Array1::<f64>::zeros(2);
4071 let refusal =
4072 match upload_shared_pirls_gpu(x.view(), y.view(), prior_w.view(), offset.view()) {
4073 Ok(_) => panic!(
4074 "no CUDA runtime on this host, yet the device-resident PIRLS upload \
4075 returned Ok — the seam fabricated device state (#1551 class)"
4076 ),
4077 Err(reason) => reason,
4078 };
4079 assert!(
4080 refusal.contains("cannot upload shared GPU PIRLS data"),
4081 "the device-free refusal must name the device-absence reason, got: {refusal}"
4082 );
4083 }
4084
4085 fn xtwx_plus_penalty(
4089 x: ndarray::ArrayView2<'_, f64>,
4090 w: ndarray::ArrayView1<'_, f64>,
4091 s: ndarray::ArrayView2<'_, f64>,
4092 ) -> ndarray::Array2<f64> {
4093 let (n, p) = x.dim();
4094 let mut h = ndarray::Array2::<f64>::zeros((p, p));
4095 for k in 0..n {
4096 for i in 0..p {
4097 for j in 0..p {
4098 h[[i, j]] += w[k] * x[[k, i]] * x[[k, j]];
4099 }
4100 }
4101 }
4102 h += &s;
4103 h
4104 }
4105
4106 fn logdet_3x3(a: ndarray::ArrayView2<'_, f64>) -> f64 {
4110 let det = a[[0, 0]] * (a[[1, 1]] * a[[2, 2]] - a[[1, 2]] * a[[2, 1]])
4111 - a[[0, 1]] * (a[[1, 0]] * a[[2, 2]] - a[[1, 2]] * a[[2, 0]])
4112 + a[[0, 2]] * (a[[1, 0]] * a[[2, 1]] - a[[1, 1]] * a[[2, 0]]);
4113 det.ln()
4114 }
4115
4116 fn assert_one_shot_step_matches_host_oracle(
4123 x: ndarray::ArrayView2<'_, f64>,
4124 weights: ndarray::ArrayView1<'_, f64>,
4125 penalty: ndarray::ArrayView2<'_, f64>,
4126 gradient: ndarray::ArrayView1<'_, f64>,
4127 step_lm_lambda: f64,
4128 ) -> PirlsGpuStep {
4129 let p = x.ncols();
4130 assert_eq!(p, 3, "the closed-form 3×3 logdet oracle fixes p = 3");
4131 let step = solve_pirls_step_gpu(PirlsGpuInput {
4132 x,
4133 weights,
4134 penalty_hessian: penalty,
4135 gradient,
4136 step_lm_lambda,
4137 objective_ridge: 0.0,
4138 })
4139 .expect("the one-shot PIRLS step entry must succeed on every host");
4140
4141 let h_ref = xtwx_plus_penalty(x, weights, penalty);
4142 let mut max_h = 0.0_f64;
4143 for i in 0..p {
4144 for j in 0..p {
4145 max_h = max_h.max((step.penalized_hessian[[i, j]] - h_ref[[i, j]]).abs());
4146 }
4147 }
4148 assert!(
4149 max_h <= 1e-12,
4150 "exported penalized Hessian must equal XᵀWX + S: max |Δ| = {max_h:.3e}"
4151 );
4152
4153 let mut h_step = h_ref;
4154 for i in 0..p {
4155 h_step[[i, i]] += step_lm_lambda;
4156 }
4157 let residual = h_step.dot(&step.direction) - &gradient.to_owned();
4158 let max_residual = residual.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
4159 assert!(
4160 max_residual <= 1e-11,
4161 "Newton direction must solve (H + λ_lm·I)·δ = rhs: max |residual| = \
4162 {max_residual:.3e}"
4163 );
4164
4165 let logdet_ref = logdet_3x3(h_step.view());
4166 assert!(
4167 (step.logdet - logdet_ref).abs() <= 1e-11,
4168 "logdet must equal ln det(H + λ_lm·I): got {} vs cofactor oracle {logdet_ref}",
4169 step.logdet
4170 );
4171 step
4172 }
4173
4174 #[test]
4181 fn device_input_step_matches_host_input_step_on_v100() {
4182 let x = arr2(&[
4183 [1.0, 0.5, 0.1],
4184 [0.2, -0.3, 1.4],
4185 [0.7, 1.1, -0.2],
4186 [-0.4, 0.9, 0.6],
4187 [0.3, -0.8, 0.5],
4188 ]);
4189 let weights = ndarray::arr1(&[1.0, 0.8, 1.2, 0.9, 1.05]);
4190 let g_eta = ndarray::arr1(&[0.10_f64, -0.20, 0.05, 0.30, -0.15]);
4194 let gradient: ndarray::Array1<f64> = x.t().dot(&g_eta);
4195 let penalty = arr2(&[[0.4, 0.0, 0.0], [0.0, 0.9, 0.0], [0.0, 0.0, 1.2]]);
4196 let lm_ridge = 0.1;
4197
4198 drop(assert_one_shot_step_matches_host_oracle(
4202 x.view(),
4203 weights.view(),
4204 penalty.view(),
4205 gradient.view(),
4206 lm_ridge,
4207 ));
4208
4209 if !device_available() {
4210 assert_device_seam_declines_without_cuda();
4214 return;
4215 }
4216
4217 let n = x.nrows();
4218 let y_dummy = ndarray::Array1::<f64>::zeros(n);
4219 let prior_w_dummy = ndarray::Array1::<f64>::ones(n);
4220 let offset_dummy = ndarray::Array1::<f64>::zeros(n);
4221 let shared = upload_shared_pirls_gpu(
4222 x.view(),
4223 y_dummy.view(),
4224 prior_w_dummy.view(),
4225 offset_dummy.view(),
4226 )
4227 .expect("upload shared design");
4228 let mut ws_host = allocate_sigma_pirls_workspace(&shared).expect("alloc host-input ws");
4229 let mut ws_dev = allocate_sigma_pirls_workspace(&shared).expect("alloc device-input ws");
4230
4231 let host_step = solve_pirls_step_on_stream(
4232 &shared,
4233 &mut ws_host,
4234 PirlsStepStreamInput {
4235 weights: weights.view(),
4236 penalty_hessian: penalty.view(),
4237 gradient: gradient.view(),
4238 step_lm_lambda: lm_ridge,
4239 objective_ridge: 0.0,
4240 },
4241 )
4242 .expect("host-input step");
4243
4244 let mut w_dev = ws_dev.stream.alloc_zeros::<f64>(n).expect("alloc w_dev");
4245 let mut g_dev = ws_dev.stream.alloc_zeros::<f64>(n).expect("alloc g_dev");
4246 ws_dev
4247 .stream
4248 .memcpy_htod(weights.as_slice().unwrap(), &mut w_dev)
4249 .expect("upload w_dev");
4250 ws_dev
4251 .stream
4252 .memcpy_htod(g_eta.as_slice().unwrap(), &mut g_dev)
4253 .expect("upload g_dev");
4254
4255 let beta_dev_test = ws_dev
4256 .stream
4257 .alloc_zeros::<f64>(x.ncols())
4258 .expect("alloc beta_dev_test");
4259 let linear_shift_test = ndarray::Array1::<f64>::zeros(x.ncols());
4260 let dev_step = solve_pirls_step_on_stream_device(
4261 &shared,
4262 &mut ws_dev,
4263 PirlsStepStreamDeviceInput {
4264 w_solver_dev: &w_dev,
4265 grad_eta_dev: &g_dev,
4266 penalty_hessian: penalty.view(),
4267 step_lm_lambda: lm_ridge,
4268 objective_ridge: 0.0,
4269 beta_dev: &beta_dev_test,
4270 linear_shift: linear_shift_test.view(),
4271 },
4272 )
4273 .expect("device-input step");
4274
4275 for i in 0..3 {
4278 for j in 0..3 {
4279 let diff = (host_step.penalized_hessian[[i, j]]
4280 - dev_step.penalized_hessian[[i, j]])
4281 .abs();
4282 assert!(diff <= 1e-10, "H[{i},{j}] mismatch: {diff}");
4283 }
4284 }
4285 assert!(
4286 (host_step.logdet - dev_step.logdet).abs() <= 1e-9,
4287 "logdet mismatch: host={} dev={}",
4288 host_step.logdet,
4289 dev_step.logdet
4290 );
4291 for i in 0..3 {
4294 let diff = (host_step.direction[i] - dev_step.direction[i]).abs();
4295 assert!(diff <= 1e-9, "direction[{i}] mismatch: {diff}");
4296 }
4297 }
4298
4299 fn logit_hill_climb_fixture(
4305 n: usize,
4306 p: usize,
4307 ) -> (
4308 ndarray::Array2<f64>,
4309 ndarray::Array1<f64>,
4310 ndarray::Array1<f64>,
4311 ndarray::Array2<f64>,
4312 ) {
4313 let beta_true: ndarray::Array1<f64> = ndarray::Array1::from_iter(
4314 (0..p).map(|j| 0.05 * ((j as f64) - 0.5 * p as f64) / p as f64),
4315 );
4316 let mut x = ndarray::Array2::<f64>::zeros((n, p));
4317 for i in 0..n {
4318 for j in 0..p {
4319 x[[i, j]] = ((i as f64 + j as f64 * 17.0) * 0.001).sin();
4320 }
4321 }
4322 let eta: ndarray::Array1<f64> = x.dot(&beta_true);
4323 let y: ndarray::Array1<f64> = eta
4324 .iter()
4325 .enumerate()
4326 .map(|(i, &e)| {
4327 let mu = 0.5 * (1.0 + (0.5 * e).tanh());
4328 if (i as f64 * 1.31).fract() < mu {
4329 1.0
4330 } else {
4331 0.0
4332 }
4333 })
4334 .collect();
4335 let prior_w = ndarray::Array1::<f64>::ones(n);
4336 let penalty = ndarray::Array2::<f64>::eye(p) * 1e-3;
4337 (x, y, prior_w, penalty)
4338 }
4339
4340 fn cpu_pirls_reference_loop(
4352 x: ndarray::ArrayView2<'_, f64>,
4353 y: ndarray::ArrayView1<'_, f64>,
4354 prior_w: ndarray::ArrayView1<'_, f64>,
4355 penalty: ndarray::ArrayView2<'_, f64>,
4356 iterations: usize,
4357 ) -> ndarray::Array1<f64> {
4358 use crate::gpu_kernels::pirls_row::{
4359 CurvatureMode, PirlsRowFamily, RowInput, row_reweight_cpu,
4360 };
4361 use gam_linalg::faer_ndarray::FaerCholesky;
4362 let (n, p) = x.dim();
4363 let mut beta = ndarray::Array1::<f64>::zeros(p);
4364 for _ in 0..iterations {
4365 let eta: ndarray::Array1<f64> = x.dot(&beta);
4366 let mut w = ndarray::Array1::<f64>::zeros(n);
4367 let mut g = ndarray::Array1::<f64>::zeros(n);
4368 for i in 0..n {
4369 let out = row_reweight_cpu(
4370 PirlsRowFamily::BernoulliLogit,
4371 CurvatureMode::Fisher,
4372 RowInput {
4373 eta: eta[i],
4374 y: y[i],
4375 prior_weight: prior_w[i],
4376 },
4377 1.0,
4378 )
4379 .expect("CPU PIRLS benchmark row must be representable");
4380 w[i] = out.w_solver;
4381 g[i] = out.grad_eta;
4382 }
4383 let mut wx_full = x.to_owned();
4384 for j in 0..p {
4385 for i in 0..n {
4386 wx_full[[i, j]] *= w[i];
4387 }
4388 }
4389 let h = x.t().dot(&wx_full) + &penalty;
4390 let rhs = x.t().dot(&g) - penalty.dot(&beta);
4394 let chol = h
4395 .cholesky(faer::Side::Lower)
4396 .expect("CPU PIRLS reference Cholesky");
4397 let d = chol.solvevec(&rhs);
4398 for i in 0..p {
4399 beta[i] += d[i];
4400 }
4401 }
4402 beta
4403 }
4404
4405 fn penalized_score_inf_norm(
4409 x: ndarray::ArrayView2<'_, f64>,
4410 y: ndarray::ArrayView1<'_, f64>,
4411 prior_w: ndarray::ArrayView1<'_, f64>,
4412 penalty: ndarray::ArrayView2<'_, f64>,
4413 beta: ndarray::ArrayView1<'_, f64>,
4414 ) -> f64 {
4415 use crate::gpu_kernels::pirls_row::{
4416 CurvatureMode, PirlsRowFamily, RowInput, row_reweight_cpu,
4417 };
4418 let eta: ndarray::Array1<f64> = x.dot(&beta);
4419 let mut g = ndarray::Array1::<f64>::zeros(x.nrows());
4420 for i in 0..x.nrows() {
4421 g[i] = row_reweight_cpu(
4422 PirlsRowFamily::BernoulliLogit,
4423 CurvatureMode::Fisher,
4424 RowInput {
4425 eta: eta[i],
4426 y: y[i],
4427 prior_weight: prior_w[i],
4428 },
4429 1.0,
4430 )
4431 .expect("penalized-score row must be representable")
4432 .grad_eta;
4433 }
4434 let score = x.t().dot(&g) - penalty.dot(&beta.to_owned());
4435 score.iter().fold(0.0_f64, |m, v| m.max(v.abs()))
4436 }
4437
4438 #[test]
4446 fn hill_climb_loop_declines_without_device_else_beats_cpu_on_large_scale_logit() {
4447 use std::time::Instant;
4448 let p = 44_usize;
4449
4450 {
4455 let (x_small, y_small, prior_w_small, penalty_small) =
4456 logit_hill_climb_fixture(4_000, p);
4457 let beta_small = cpu_pirls_reference_loop(
4458 x_small.view(),
4459 y_small.view(),
4460 prior_w_small.view(),
4461 penalty_small.view(),
4462 30,
4463 );
4464 assert!(
4465 beta_small.iter().all(|v| v.is_finite()),
4466 "CPU PIRLS baseline diverged to a non-finite β at n=4000"
4467 );
4468 let score = penalized_score_inf_norm(
4469 x_small.view(),
4470 y_small.view(),
4471 prior_w_small.view(),
4472 penalty_small.view(),
4473 beta_small.view(),
4474 );
4475 assert!(
4476 score <= 1e-6,
4477 "CPU PIRLS baseline did not reach the penalized MLE at n=4000: \
4478 ‖Xᵀg − Sβ‖∞ = {score:.3e}"
4479 );
4480 }
4481
4482 if !device_available() {
4483 assert_device_seam_declines_without_cuda();
4485 return;
4486 }
4487
4488 use crate::gpu_kernels::pirls_row::{CurvatureMode, PirlsRowFamily};
4489 let n = 80_000_usize;
4490 let (x, y, prior_w, penalty) = logit_hill_climb_fixture(n, p);
4491 let beta0 = ndarray::Array1::<f64>::zeros(p);
4492
4493 let offset_bench = ndarray::Array1::<f64>::zeros(n);
4495 let shared =
4496 upload_shared_pirls_gpu(x.view(), y.view(), prior_w.view(), offset_bench.view())
4497 .expect("upload shared design");
4498 let mut ws = allocate_sigma_pirls_workspace(&shared).expect("alloc ws");
4499 let mut loop_ws = allocate_pirls_loop_workspace(&shared, &ws).expect("alloc loop_ws");
4500
4501 {
4511 let warm_shift = ndarray::Array1::<f64>::zeros(p);
4512 drop(
4513 pirls_loop_on_stream(
4514 &shared,
4515 &mut ws,
4516 &mut loop_ws,
4517 PirlsRowFamily::BernoulliLogit,
4518 CurvatureMode::Fisher,
4519 PirlsLoopLikelihoodScale::non_gamma(),
4520 beta0.view(),
4521 penalty.view(),
4522 warm_shift.view(),
4523 0.0,
4524 0.0,
4525 0.0,
4526 30,
4527 1e-6,
4528 None,
4529 )
4530 .expect("warmup pirls loop"),
4531 );
4532 }
4533
4534 let t0 = Instant::now();
4535 let linear_shift_zero = ndarray::Array1::<f64>::zeros(p);
4539 let gpu_outcome = pirls_loop_on_stream(
4540 &shared,
4541 &mut ws,
4542 &mut loop_ws,
4543 PirlsRowFamily::BernoulliLogit,
4544 CurvatureMode::Fisher,
4545 PirlsLoopLikelihoodScale::non_gamma(),
4546 beta0.view(),
4547 penalty.view(),
4548 linear_shift_zero.view(),
4549 0.0,
4550 0.0,
4551 0.0,
4552 30,
4553 1e-6,
4554 None,
4555 )
4556 .expect("pirls loop");
4557 let gpu_secs = t0.elapsed().as_secs_f64();
4558
4559 let iterations = gpu_outcome.iterations.max(1);
4569
4570 let t1 = Instant::now();
4573 let beta_cpu = cpu_pirls_reference_loop(
4574 x.view(),
4575 y.view(),
4576 prior_w.view(),
4577 penalty.view(),
4578 iterations,
4579 );
4580 let cpu_secs = t1.elapsed().as_secs_f64();
4581
4582 assert!(
4586 gpu_outcome.beta.iter().all(|v| v.is_finite()),
4587 "device PIRLS loop returned a non-finite β at n={n}"
4588 );
4589 let mut max_beta_delta = 0.0_f64;
4590 for i in 0..p {
4591 max_beta_delta = max_beta_delta.max((gpu_outcome.beta[i] - beta_cpu[i]).abs());
4592 }
4593 let gpu_score = penalized_score_inf_norm(
4594 x.view(),
4595 y.view(),
4596 prior_w.view(),
4597 penalty.view(),
4598 gpu_outcome.beta.view(),
4599 );
4600
4601 let speedup = cpu_secs / gpu_secs;
4602 eprintln!(
4603 "[hill_climb] n={n} p={p} BernoulliLogit/Fisher: gpu={:.3}s cpu={:.3}s \
4604 speedup={:.2}× iters={iterations} converged={} max|Δβ|={max_beta_delta:.3e} \
4605 gpu ‖Xᵀg − Sβ‖∞={gpu_score:.3e}",
4606 gpu_secs, cpu_secs, speedup, gpu_outcome.converged
4607 );
4608 assert!(
4609 max_beta_delta <= 1e-8,
4610 "iteration-matched device-vs-CPU PIRLS β parity at n={n}: max |Δβ| = \
4611 {max_beta_delta:.3e} after {iterations} shared iterations"
4612 );
4613 assert!(
4614 gpu_score <= 1e-6,
4615 "device PIRLS loop did not reach the penalized MLE at n={n}: \
4616 ‖Xᵀg − Sβ‖∞ = {gpu_score:.3e}"
4617 );
4618 assert!(
4624 speedup >= 2.0,
4625 "GPU PIRLS loop dispatch-worthiness: got speedup={speedup:.2}× \
4626 (gpu={gpu_secs:.3}s cpu={cpu_secs:.3}s, both over {iterations} iterations) \
4627 — the resident loop must clearly beat the same-box CPU. #2424: this ratio \
4628 is iteration-matched. The previous form timed a fixed 30 CPU iterations \
4629 against a device loop that converges in 3, so it reported ~4× where the \
4630 per-iteration truth is below 1×"
4631 );
4632 }
4633
4634 #[test]
4645 fn pirls_loop_converges_to_ols_solution_on_gaussian_identity() {
4646 let x = arr2(&[
4647 [1.0, 0.5, 0.1],
4648 [0.2, -0.3, 1.4],
4649 [0.7, 1.1, -0.2],
4650 [-0.4, 0.9, 0.6],
4651 [0.3, -0.8, 0.5],
4652 [1.1, 0.2, -0.4],
4653 [-0.6, 0.4, 0.3],
4654 [0.8, -1.0, 0.7],
4655 ]);
4656 let n = x.nrows();
4657 let p = x.ncols();
4658 let beta_true = ndarray::arr1(&[0.5_f64, -1.2, 0.3]);
4660 let y: ndarray::Array1<f64> = x.dot(&beta_true);
4661 let prior_w = ndarray::Array1::<f64>::ones(n);
4662 let penalty = ndarray::Array2::<f64>::eye(p) * 1e-4; let beta0 = ndarray::Array1::<f64>::zeros(p);
4664
4665 let xtx = x.t().dot(&x);
4667 let xty = x.t().dot(&y);
4668 let h_ref = xtx + &penalty;
4669 use gam_linalg::faer_ndarray::FaerCholesky;
4671 let chol = h_ref
4672 .cholesky(faer::Side::Lower)
4673 .expect("OLS reference Cholesky");
4674 let beta_ref: ndarray::Array1<f64> = chol.solvevec(&xty);
4675
4676 let one_shot = assert_one_shot_step_matches_host_oracle(
4682 x.view(),
4683 prior_w.view(),
4684 penalty.view(),
4685 xty.view(),
4686 0.0,
4687 );
4688 let mut max_beta_delta = 0.0_f64;
4689 for i in 0..p {
4690 max_beta_delta = max_beta_delta.max((one_shot.direction[i] - beta_ref[i]).abs());
4691 }
4692 assert!(
4693 max_beta_delta <= 1e-9,
4694 "one-shot Gaussian-identity step must equal the closed-form ridge OLS \
4695 solution: max |Δβ| = {max_beta_delta:.3e}"
4696 );
4697
4698 if !device_available() {
4699 assert_device_seam_declines_without_cuda();
4702 return;
4703 }
4704
4705 let offset_ols = ndarray::Array1::<f64>::zeros(n);
4706 let shared = upload_shared_pirls_gpu(x.view(), y.view(), prior_w.view(), offset_ols.view())
4707 .expect("upload shared design");
4708 let mut ws = allocate_sigma_pirls_workspace(&shared).expect("alloc ws");
4709 let mut loop_ws = allocate_pirls_loop_workspace(&shared, &ws).expect("alloc loop_ws");
4710
4711 let linear_shift_zero = ndarray::Array1::<f64>::zeros(p);
4715 let outcome = pirls_loop_on_stream(
4716 &shared,
4717 &mut ws,
4718 &mut loop_ws,
4719 crate::gpu_kernels::pirls_row::PirlsRowFamily::GaussianIdentity,
4720 crate::gpu_kernels::pirls_row::CurvatureMode::Fisher,
4721 PirlsLoopLikelihoodScale::non_gamma(),
4722 beta0.view(),
4723 penalty.view(),
4724 linear_shift_zero.view(),
4725 0.0,
4726 0.0,
4727 0.0,
4728 20,
4729 1e-9,
4730 None,
4731 )
4732 .expect("pirls loop");
4733
4734 assert!(
4739 outcome.converged || outcome.iterations <= 5,
4740 "PIRLS loop did not converge in 20 iters on Gaussian-identity (iters={})",
4741 outcome.iterations
4742 );
4743 for i in 0..p {
4744 let diff = (outcome.beta[i] - beta_ref[i]).abs();
4745 assert!(
4746 diff <= 1e-6,
4747 "β[{i}] mismatch: gpu={} ref={} diff={}",
4748 outcome.beta[i],
4749 beta_ref[i],
4750 diff
4751 );
4752 }
4753 for i in 0..p {
4756 for j in 0..p {
4757 let diff = (outcome.penalized_hessian[[i, j]] - h_ref[[i, j]]).abs();
4758 assert!(diff <= 1e-8, "H[{i},{j}] mismatch: {diff}");
4759 }
4760 }
4761 }
4762}
4763
4764#[cfg(test)]
4773mod weighted_crossprod_cpu_fallback_tests {
4774 use super::weighted_crossprod_gpu;
4775 use ndarray::{Array1, Array2};
4776
4777 #[test]
4778 fn weighted_crossprod_gpu_cpu_fallback_matches_dense_xtwx() {
4779 let x = Array2::<f64>::from_shape_fn((4, 3), |(i, j)| (i + j) as f64 + 1.0);
4782 let w = Array1::<f64>::from_vec(vec![0.5, 1.0, 1.5, 2.0]);
4783
4784 let got = weighted_crossprod_gpu(x.view(), w.view())
4785 .expect("weighted_crossprod_gpu must return Ok via CPU fallback on a CPU-only host");
4786
4787 let (n, p) = x.dim();
4789 let mut expected = Array2::<f64>::zeros((p, p));
4790 for k in 0..n {
4791 for i in 0..p {
4792 for j in 0..p {
4793 expected[[i, j]] += w[k] * x[[k, i]] * x[[k, j]];
4794 }
4795 }
4796 }
4797
4798 assert_eq!(got.dim(), (p, p));
4799 for i in 0..p {
4800 for j in 0..p {
4801 let diff = (got[[i, j]] - expected[[i, j]]).abs();
4802 assert!(
4803 diff <= 1e-10,
4804 "XtWX[{i},{j}] mismatch: got vs expected diff={diff}"
4805 );
4806 }
4807 }
4808 }
4809}