1use ndarray::{Array1, Array2, Array3, ArrayView1, ArrayView2, ArrayView3};
23
24use super::device_runtime::GpuRuntime;
25use super::policy::GpuDispatchPolicy;
26use super::GpuPolicy;
27
28pub struct CudaGemmDispatch;
29
30impl gam_linalg::gpu_hook::GpuGemmDispatch for CudaGemmDispatch {
31 fn try_fast_atb(&self, a: ArrayView2<'_, f64>, b: ArrayView2<'_, f64>) -> Option<Array2<f64>> {
32 try_fast_atb(a, b)
33 }
34
35 fn try_fast_ab(&self, a: ArrayView2<'_, f64>, b: ArrayView2<'_, f64>) -> Option<Array2<f64>> {
36 try_fast_ab(a, b)
37 }
38
39 fn try_fast_av(&self, a: ArrayView2<'_, f64>, v: ArrayView1<'_, f64>) -> Option<Array1<f64>> {
40 try_fast_av(a, v)
41 }
42
43 fn try_fast_atv(&self, a: ArrayView2<'_, f64>, v: ArrayView1<'_, f64>) -> Option<Array1<f64>> {
44 try_fast_atv(a, v)
45 }
46
47 fn try_fast_xt_diag_x(
48 &self,
49 x: ArrayView2<'_, f64>,
50 w: ArrayView1<'_, f64>,
51 ) -> Option<Array2<f64>> {
52 try_fast_xt_diag_x(x, w)
53 }
54
55 fn try_fast_xt_diag_y(
56 &self,
57 x: ArrayView2<'_, f64>,
58 w: ArrayView1<'_, f64>,
59 y: ArrayView2<'_, f64>,
60 ) -> Option<Array2<f64>> {
61 try_fast_xt_diag_y(x, w, y)
62 }
63
64 fn try_fast_joint_hessian_2x2(
65 &self,
66 x_a: ArrayView2<'_, f64>,
67 x_b: ArrayView2<'_, f64>,
68 w_aa: ArrayView1<'_, f64>,
69 w_ab: ArrayView1<'_, f64>,
70 w_bb: ArrayView1<'_, f64>,
71 ) -> Option<Array2<f64>> {
72 try_fast_joint_hessian_2x2(x_a, x_b, w_aa, w_ab, w_bb)
73 }
74
75 fn device_count(&self) -> usize {
76 let policy = super::global_policy();
77 runtime_for_dispatch(policy).map_or(0, GpuRuntime::device_count)
78 }
79
80 fn try_fast_ab_broadcast_b_batched(
81 &self,
82 a3: ArrayView3<'_, f64>,
83 b: ArrayView2<'_, f64>,
84 ) -> Option<Array3<f64>> {
85 try_fast_ab_broadcast_b_batched(a3, b)
86 }
87}
88
89#[derive(Clone, Copy, Debug)]
92pub enum DispatchOp {
93 Gemm { m: usize, n: usize, k: usize },
95 BatchedGemm {
97 batch: usize,
98 m: usize,
99 n: usize,
100 k: usize,
101 },
102 Potrf { p: usize, batch: usize },
104 SmallDenseBatchedPotrf { p: usize, batch: usize },
109 Trsm { m: usize, n: usize },
111 Gemv { m: usize, k: usize },
113 XtDiagX { n: usize, p: usize },
115 XtDiagY { n: usize, px: usize, q: usize },
117 JointHessian2x2 { n: usize, pa: usize, pb: usize },
119}
120
121#[inline]
126fn runtime_for_dispatch(policy: GpuPolicy) -> Option<&'static GpuRuntime> {
127 GpuRuntime::resolve(policy).unwrap_or_else(|error| {
128 panic!(
132 "GPU runtime resolution failed under policy '{}': {error}",
133 policy
134 )
135 })
136}
137
138#[inline]
143#[track_caller]
144fn decline_gpu<T>(operation: &'static str, reason: &'static str) -> Option<T> {
145 if super::global_policy() == GpuPolicy::Required {
146 panic!("gpu=required operation '{operation}' cannot execute on the GPU: {reason}");
149 }
150 None
151}
152
153#[inline]
157#[track_caller]
158fn invalid_gpu_request(operation: &'static str, reason: &'static str) -> ! {
159 panic!("GPU operation '{operation}' received invalid input: {reason}");
162}
163
164#[cfg(not(target_os = "linux"))]
171#[inline]
172#[track_caller]
173fn decline_gpu_with_policy<T>(
174 operation: &'static str,
175 reason: &'static str,
176 gpu_policy: GpuPolicy,
177) -> Option<T> {
178 if gpu_policy == GpuPolicy::Required {
179 panic!("gpu=required operation '{operation}' cannot execute on the GPU: {reason}");
182 }
183 None
184}
185
186 #[cfg(target_os = "linux")]
190#[inline]
191#[track_caller]
192fn invalid_gpu_result(operation: &'static str, reason: &'static str) -> ! {
193 panic!("GPU operation '{operation}' produced invalid output: {reason}");
196}
197
198#[cfg(target_os = "linux")]
203#[inline]
204#[track_caller]
205fn complete_gpu_attempt<T>(operation: &'static str, result: Option<T>) -> T {
206 match result {
207 Some(value) => value,
208 None => panic!(
212 "GPU operation '{operation}' failed after admission under policy '{}'",
213 super::global_policy()
214 ),
215 }
216}
217
218impl DispatchOp {
219 #[inline]
221 pub const fn flops(self) -> u128 {
222 match self {
223 Self::Gemm { m, n, k } => 2u128 * (m as u128) * (n as u128) * (k as u128),
224 Self::BatchedGemm { batch, m, n, k } => {
225 2u128 * (batch as u128) * (m as u128) * (n as u128) * (k as u128)
226 }
227 Self::Gemv { m, k } => 2u128 * (m as u128) * (k as u128),
228 Self::Potrf { p, batch } => (batch as u128) * (p as u128).pow(3) / 3,
229 Self::SmallDenseBatchedPotrf { p, batch } => (batch as u128) * (p as u128).pow(3) / 3,
230 Self::Trsm { m, n } => (m as u128) * (m as u128) * (n as u128),
231 Self::XtDiagX { n, p } => 2u128 * (n as u128) * (p as u128) * (p as u128),
232 Self::XtDiagY { n, px, q } => 2u128 * (n as u128) * (px as u128) * (q as u128),
233 Self::JointHessian2x2 { n, pa, pb } => {
234 let total = (pa as u128) + (pb as u128);
235 2u128 * (n as u128) * total * total
236 }
237 }
238 }
239
240 #[must_use]
255 pub fn admissible_under_any_policy(self) -> bool {
256 let seed = GpuDispatchPolicy::default();
257 let min_gemm = GpuDispatchPolicy::MIN_CALIBRATABLE_GEMM_FLOPS;
258 match self {
259 Self::Gemm { m, n, k } => self.flops() >= min_gemm && m.min(n).min(k) > 0,
260 Self::BatchedGemm { batch, m, n, k } => {
261 self.flops() >= min_gemm && batch > 1 && m.min(n).min(k) > 0
262 }
263 Self::Gemv { m, k } => self.flops() >= min_gemm && m > 0 && k > 0,
264 Self::Potrf { p, batch } => {
265 p > 0
266 && batch > 0
267 && (p >= GpuDispatchPolicy::MIN_CALIBRATABLE_POTRF_P
268 || (batch > 1 && self.flops() >= min_gemm))
269 }
270 Self::SmallDenseBatchedPotrf { p, batch } => {
273 p > 0
274 && p <= seed.small_dense_batched_potrf_max_p
275 && batch >= seed.small_dense_batched_potrf_min_batch
276 }
277 Self::Trsm { m, n } => self.flops() >= min_gemm && m > 0 && n > 0,
278 Self::XtDiagX { n, p } => n > 0 && p > 0 && self.flops() >= min_gemm,
283 Self::XtDiagY { n, px, q } => n > 0 && px > 0 && q > 0 && self.flops() >= min_gemm,
284 Self::JointHessian2x2 { n, pa, pb } => {
285 n > 0 && (pa > 0 || pb > 0) && self.flops() >= min_gemm
286 }
287 }
288 }
289}
290
291#[inline]
296#[must_use]
297pub fn route_through_gpu(op: DispatchOp) -> Option<&'static GpuRuntime> {
298 route_through_gpu_with_policy(op, super::global_policy())
299}
300
301#[inline]
305#[must_use]
306pub fn route_through_gpu_with_policy(
307 op: DispatchOp,
308 selected_policy: GpuPolicy,
309) -> Option<&'static GpuRuntime> {
310 if selected_policy != GpuPolicy::Required && !op.admissible_under_any_policy() {
317 return None;
318 }
319 let runtime = runtime_for_dispatch(selected_policy)?;
320 if selected_policy == GpuPolicy::Required {
321 return Some(runtime);
322 }
323 let policy = &runtime.policy;
324 let admit = match op {
325 DispatchOp::Gemm { m, n, k } => {
326 op.flops() >= (policy.gemm_min_flops as u128) && m.min(n).min(k) > 0
327 }
328 DispatchOp::BatchedGemm { batch, m, n, k } => {
329 op.flops() >= (policy.gemm_min_flops as u128) && batch > 1 && m.min(n).min(k) > 0
330 }
331 DispatchOp::Gemv { m, k } => {
332 op.flops() >= (policy.gemm_min_flops as u128) && m > 0 && k > 0
333 }
334 DispatchOp::Potrf { p, batch } => {
335 p > 0
336 && batch > 0
337 && (p >= policy.potrf_min_p
338 || (batch > 1 && op.flops() >= policy.gemm_min_flops as u128))
339 }
340 DispatchOp::SmallDenseBatchedPotrf { p, batch } => {
341 p > 0
342 && p <= policy.small_dense_batched_potrf_max_p
343 && batch >= policy.small_dense_batched_potrf_min_batch
344 }
345 DispatchOp::Trsm { m, n } => {
346 op.flops() >= (policy.gemm_min_flops as u128) && m > 0 && n > 0
347 }
348 DispatchOp::XtDiagX { n, p } => policy.xtwx_target_is_gpu(n, p, true),
349 DispatchOp::XtDiagY { n, px, q } => policy.xtwy_target_is_gpu(n, px, q, true),
350 DispatchOp::JointHessian2x2 { n, pa, pb } => {
351 n > 0 && (pa > 0 || pb > 0) && op.flops() >= policy.gemm_min_flops as u128
352 }
353 };
354 if admit { Some(runtime) } else { None }
355}
356
357#[cfg(target_os = "linux")]
364const MULTI_GPU_BATCH_FLOOR: usize = 64;
365
366#[cfg(target_os = "linux")]
369#[inline]
370fn should_split_batch(batch: usize) -> bool {
371 let policy = super::global_policy();
372 runtime_for_dispatch(policy).is_some_and(|rt| rt.device_count() > 1)
373 && batch >= MULTI_GPU_BATCH_FLOOR
374}
375
376#[inline]
377#[must_use]
378pub fn try_fast_ab_broadcast_b_batched(
379 a: ArrayView3<'_, f64>,
380 b: ArrayView2<'_, f64>,
381) -> Option<Array3<f64>> {
382 let (batch, m, k) = a.dim();
383 let (bk, n) = b.dim();
384 if k != bk {
385 invalid_gpu_request("batched A·B", "the reduction dimensions differ");
386 }
387 if batch == 0 || m == 0 || n == 0 || k == 0 {
388 return decline_gpu(
389 "batched A·B",
390 "the workload has an empty dimension",
391 );
392 }
393 #[cfg(not(target_os = "linux"))]
394 {
395 return decline_gpu("batched A·B", "the CUDA backend is not compiled on this platform");
396 }
397 #[cfg(target_os = "linux")]
398 {
399 let runtime = route_through_gpu(DispatchOp::BatchedGemm { batch, m, n, k })?;
400 if should_split_batch(batch) {
401 if let Some(out) = scatter_broadcast_b_batched(runtime, a, b, m, n) {
402 return Some(out);
403 }
404 }
407 Some(complete_gpu_attempt(
408 "batched A·B",
409 cuda_backend::gemm_broadcast_b_batched(runtime.device.ordinal, a, b),
410 ))
411 }
412}
413
414#[cfg(target_os = "linux")]
420fn scatter_broadcast_b_batched(
421 runtime: &GpuRuntime,
422 a: ArrayView3<'_, f64>,
423 b: ArrayView2<'_, f64>,
424 m: usize,
425 n: usize,
426) -> Option<Array3<f64>> {
427 let batch = a.dim().0;
428 let mut items: Vec<(Array2<f64>, Option<Array2<f64>>)> = (0..batch)
431 .map(|i| (a.index_axis(ndarray::Axis(0), i).to_owned(), None))
432 .collect();
433 super::pool::scatter_batched(runtime, &mut items, |ordinal, tile| {
434 let tile_batch = tile.len();
435 if tile_batch == 0 {
436 return Some(());
437 }
438 let k = b.dim().0;
439 let mut a_tile = Array3::<f64>::zeros((tile_batch, m, k));
440 for (idx, (a_i, _)) in tile.iter().enumerate() {
441 a_tile.index_axis_mut(ndarray::Axis(0), idx).assign(a_i);
442 }
443 let out = cuda_backend::gemm_broadcast_b_batched(ordinal, a_tile.view(), b)?;
444 for (idx, (_, slot)) in tile.iter_mut().enumerate() {
445 *slot = Some(out.index_axis(ndarray::Axis(0), idx).to_owned());
446 }
447 Some(())
448 })?;
449 stitch_batched(items, m, n)
450}
451
452#[inline]
453#[must_use]
454pub fn try_fast_abt_strided_batched(
455 a: ArrayView3<'_, f64>,
456 b: ArrayView3<'_, f64>,
457) -> Option<Array3<f64>> {
458 try_fast_abt_strided_batched_with_policy(a, b, super::global_policy())
459}
460
461#[inline]
462#[must_use]
463pub fn try_fast_abt_strided_batched_with_policy(
464 a: ArrayView3<'_, f64>,
465 b: ArrayView3<'_, f64>,
466 gpu_policy: GpuPolicy,
467) -> Option<Array3<f64>> {
468 let (batch, m, k) = a.dim();
469 let (batch_b, n, k_b) = b.dim();
470 if batch != batch_b || k != k_b {
471 invalid_gpu_request("batched A·Bᵀ", "the batch or reduction dimensions differ");
472 }
473 if batch == 0 || m == 0 || n == 0 || k == 0 {
474 return decline_gpu(
475 "batched A·Bᵀ",
476 "the workload has an empty dimension",
477 );
478 }
479 #[cfg(not(target_os = "linux"))]
480 {
481 return decline_gpu_with_policy(
482 "batched A·Bᵀ",
483 "the CUDA backend is not compiled on this platform",
484 gpu_policy,
485 );
486 }
487 #[cfg(target_os = "linux")]
488 {
489 let runtime =
490 route_through_gpu_with_policy(DispatchOp::BatchedGemm { batch, m, n, k }, gpu_policy)?;
491 if should_split_batch(batch) {
492 if let Some(out) = scatter_abt_strided_batched(runtime, a, b, m, n) {
493 return Some(out);
494 }
495 }
496 Some(complete_gpu_attempt(
497 "batched A·Bᵀ",
498 cuda_backend::gemm_abt_strided_batched(runtime.device.ordinal, a, b),
499 ))
500 }
501}
502
503#[cfg(target_os = "linux")]
508fn scatter_abt_strided_batched(
509 runtime: &GpuRuntime,
510 a: ArrayView3<'_, f64>,
511 b: ArrayView3<'_, f64>,
512 m: usize,
513 n: usize,
514) -> Option<Array3<f64>> {
515 let batch = a.dim().0;
516 let mut items: Vec<(Array2<f64>, Array2<f64>, Option<Array2<f64>>)> = (0..batch)
517 .map(|i| {
518 (
519 a.index_axis(ndarray::Axis(0), i).to_owned(),
520 b.index_axis(ndarray::Axis(0), i).to_owned(),
521 None,
522 )
523 })
524 .collect();
525 super::pool::scatter_batched(runtime, &mut items, |ordinal, tile| {
526 let tile_batch = tile.len();
527 if tile_batch == 0 {
528 return Some(());
529 }
530 let k = tile[0].0.dim().1;
531 let mut a_tile = Array3::<f64>::zeros((tile_batch, m, k));
532 let mut b_tile = Array3::<f64>::zeros((tile_batch, n, k));
533 for (idx, (a_i, b_i, _)) in tile.iter().enumerate() {
534 a_tile.index_axis_mut(ndarray::Axis(0), idx).assign(a_i);
535 b_tile.index_axis_mut(ndarray::Axis(0), idx).assign(b_i);
536 }
537 let out = cuda_backend::gemm_abt_strided_batched(ordinal, a_tile.view(), b_tile.view())?;
538 for (idx, (_, _, slot)) in tile.iter_mut().enumerate() {
539 *slot = Some(out.index_axis(ndarray::Axis(0), idx).to_owned());
540 }
541 Some(())
542 })?;
543 let slots: Vec<((), Option<Array2<f64>>)> =
544 items.into_iter().map(|(_, _, slot)| ((), slot)).collect();
545 stitch_batched(slots, m, n)
546}
547
548#[cfg(target_os = "linux")]
552fn stitch_batched<L>(
553 items: Vec<(L, Option<Array2<f64>>)>,
554 m: usize,
555 n: usize,
556) -> Option<Array3<f64>> {
557 let batch = items.len();
558 let mut out = Array3::<f64>::zeros((batch, m, n));
559 for (idx, (_, slot)) in items.into_iter().enumerate() {
560 let block = slot?;
561 if block.dim() != (m, n) {
562 return None;
563 }
564 out.index_axis_mut(ndarray::Axis(0), idx).assign(&block);
565 }
566 Some(out)
567}
568
569#[inline]
583#[must_use]
584pub fn try_fast_ab(a: ArrayView2<'_, f64>, b: ArrayView2<'_, f64>) -> Option<Array2<f64>> {
585 let (m, k) = a.dim();
586 let (kb, n) = b.dim();
587 if k != kb {
588 invalid_gpu_request("A·B", "the reduction dimensions differ");
589 }
590 if m == 0 || n == 0 || k == 0 {
591 return decline_gpu("A·B", "the workload has an empty dimension");
592 }
593 let runtime = route_through_gpu(DispatchOp::Gemm { m, n, k });
599 let used_gpu = runtime.is_some();
600 super::profile::record(super::profile::KernelStat {
601 name: "try_fast_ab",
602 n: m,
603 p: n,
604 k,
605 flops_est: (DispatchOp::Gemm { m, n, k }.flops().min(usize::MAX as u128)) as usize,
606 gpu_ms: if used_gpu { Some(0.0) } else { None },
607 ..Default::default()
608 });
609 #[cfg(not(target_os = "linux"))]
610 {
611 decline_gpu("A·B", "the CUDA backend is not compiled on this platform")
612 }
613 #[cfg(target_os = "linux")]
614 {
615 let runtime = runtime?;
616 Some(complete_gpu_attempt(
617 "A·B",
618 cuda_backend::gemm(runtime, a, b, false, false),
619 ))
620 }
621}
622
623#[inline]
624#[must_use]
625pub fn try_fast_atb(a: ArrayView2<'_, f64>, b: ArrayView2<'_, f64>) -> Option<Array2<f64>> {
626 let (n_a, p) = a.dim();
627 let (n_b, q) = b.dim();
628 if n_a != n_b {
629 invalid_gpu_request("Aᵀ·B", "the row dimensions differ");
630 }
631 if n_a == 0 || p == 0 || q == 0 {
632 return decline_gpu("Aᵀ·B", "the workload has an empty dimension");
633 }
634 #[cfg(not(target_os = "linux"))]
635 {
636 return decline_gpu("Aᵀ·B", "the CUDA backend is not compiled on this platform");
637 }
638 #[cfg(target_os = "linux")]
639 {
640 let runtime = route_through_gpu(DispatchOp::Gemm { m: p, n: q, k: n_a })?;
641 Some(complete_gpu_attempt(
642 "Aᵀ·B",
643 cuda_backend::gemm(runtime, a, b, true, false),
644 ))
645 }
646}
647
648#[inline]
657#[must_use]
658pub fn try_fast_atb_on_ordinal(
659 ordinal: usize,
660 a: ArrayView2<'_, f64>,
661 b: ArrayView2<'_, f64>,
662) -> Option<Array2<f64>> {
663 let (n_a, p) = a.dim();
664 let (n_b, q) = b.dim();
665 if n_a != n_b {
666 invalid_gpu_request("ordinal-pinned Aᵀ·B", "the row dimensions differ");
667 }
668 if n_a == 0 || p == 0 || q == 0 {
669 return decline_gpu(
670 "ordinal-pinned Aᵀ·B",
671 "the workload has an empty dimension",
672 );
673 }
674 #[cfg(not(target_os = "linux"))]
675 {
676 log::trace!(
682 "try_fast_atb_on_ordinal: CUDA unavailable off Linux; declining ordinal {ordinal}"
683 );
684 return decline_gpu(
685 "ordinal-pinned Aᵀ·B",
686 "the CUDA backend is not compiled on this platform",
687 );
688 }
689 #[cfg(target_os = "linux")]
690 {
691 route_through_gpu(DispatchOp::Gemm { m: p, n: q, k: n_a })?;
703 Some(complete_gpu_attempt(
704 "ordinal-pinned Aᵀ·B",
705 cuda_backend::gemm_on_ordinal(ordinal, a, b, true, false),
706 ))
707 }
708}
709
710#[inline]
711#[must_use]
712pub fn try_fast_av(a: ArrayView2<'_, f64>, v: ArrayView1<'_, f64>) -> Option<Array1<f64>> {
713 let (m, k) = a.dim();
714 if k != v.len() {
715 invalid_gpu_request("A·v", "the matrix width and vector length differ");
716 }
717 if m == 0 || k == 0 {
718 return decline_gpu("A·v", "the workload has an empty dimension");
719 }
720 #[cfg(not(target_os = "linux"))]
721 {
722 return decline_gpu("A·v", "the CUDA backend is not compiled on this platform");
723 }
724 #[cfg(target_os = "linux")]
725 {
726 let runtime = route_through_gpu(DispatchOp::Gemv { m, k })?;
727 Some(complete_gpu_attempt(
728 "A·v",
729 cuda_backend::gemv(runtime, a, v, false),
730 ))
731 }
732}
733
734#[inline]
735#[must_use]
736pub fn try_fast_atv(a: ArrayView2<'_, f64>, v: ArrayView1<'_, f64>) -> Option<Array1<f64>> {
737 let (n, p) = a.dim();
738 if n != v.len() {
739 invalid_gpu_request("Aᵀ·v", "the matrix height and vector length differ");
740 }
741 if n == 0 || p == 0 {
742 return decline_gpu("Aᵀ·v", "the workload has an empty dimension");
743 }
744 #[cfg(not(target_os = "linux"))]
745 {
746 return decline_gpu("Aᵀ·v", "the CUDA backend is not compiled on this platform");
747 }
748 #[cfg(target_os = "linux")]
749 {
750 let runtime = route_through_gpu(DispatchOp::Gemv { m: p, k: n })?;
751 Some(complete_gpu_attempt(
752 "Aᵀ·v",
753 cuda_backend::gemv(runtime, a, v, true),
754 ))
755 }
756}
757
758#[inline]
759#[must_use]
760pub fn try_fast_xt_diag_x(x: ArrayView2<'_, f64>, w: ArrayView1<'_, f64>) -> Option<Array2<f64>> {
761 let (n, p) = x.dim();
762 if n != w.len() {
763 invalid_gpu_request("Xᵀ·diag(w)·X", "the row and weight counts differ");
764 }
765 if n == 0 || p == 0 {
766 return decline_gpu("Xᵀ·diag(w)·X", "the workload has an empty dimension");
767 }
768 #[cfg(not(target_os = "linux"))]
769 {
770 return decline_gpu(
771 "Xᵀ·diag(w)·X",
772 "the CUDA backend is not compiled on this platform",
773 );
774 }
775 #[cfg(target_os = "linux")]
776 {
777 let runtime = route_through_gpu(DispatchOp::XtDiagX { n, p })?;
778 Some(complete_gpu_attempt(
779 "Xᵀ·diag(w)·X",
780 cuda_backend::xt_diag_x(runtime, x, w),
781 ))
782 }
783}
784
785pub struct ResidentDesignGram {
806 #[cfg(target_os = "linux")]
807 inner: super::blas::ResidentWeightedGram,
808 #[cfg(not(target_os = "linux"))]
809 _never: std::convert::Infallible,
810}
811
812impl ResidentDesignGram {
813 #[must_use]
817 pub fn try_new(x: ArrayView2<'_, f64>) -> Option<Self> {
818 let (n, p) = x.dim();
819 if n == 0 || p == 0 {
820 return decline_gpu("resident weighted Gram upload", "the design matrix is empty");
821 }
822 #[cfg(not(target_os = "linux"))]
823 {
824 decline_gpu(
825 "resident weighted Gram upload",
826 "the CUDA backend is not compiled on this platform",
827 )
828 }
829 #[cfg(target_os = "linux")]
830 {
831 let runtime = route_through_gpu(DispatchOp::XtDiagX { n, p })?;
832 let inner = complete_gpu_attempt(
833 "resident weighted Gram upload",
834 super::blas::ResidentWeightedGram::new(runtime.device.ordinal, x),
835 );
836 Some(Self { inner })
837 }
838 }
839
840 #[must_use]
844 pub fn gram(&self, w: ArrayView1<'_, f64>) -> Option<Array2<f64>> {
845 #[cfg(not(target_os = "linux"))]
846 {
847 panic!(
853 "ResidentDesignGram cannot be constructed off CUDA (w.len()={})",
854 w.len()
855 )
856 }
857 #[cfg(target_os = "linux")]
858 {
859 Some(complete_gpu_attempt(
860 "resident Xᵀ·diag(w)·X",
861 self.inner.gram(w),
862 ))
863 }
864 }
865
866 #[must_use]
879 pub fn solve_normal_equations(
880 &self,
881 w: ArrayView1<'_, f64>,
882 rhs: ArrayView1<'_, f64>,
883 ridge: f64,
884 ) -> Option<Array1<f64>> {
885 #[cfg(not(target_os = "linux"))]
886 {
887 panic!(
889 "ResidentDesignGram cannot be constructed off CUDA (w.len()={}, rhs.len()={}, ridge={ridge})",
890 w.len(),
891 rhs.len()
892 )
893 }
894 #[cfg(target_os = "linux")]
895 {
896 Some(complete_gpu_attempt(
897 "resident normal-equations solve",
898 self.inner.solve_psd_normal_equations(w, rhs, ridge),
899 ))
900 }
901 }
902
903 #[must_use]
905 pub fn dims(&self) -> (usize, usize) {
906 #[cfg(not(target_os = "linux"))]
907 {
908 panic!("ResidentDesignGram cannot be constructed off CUDA")
912 }
913 #[cfg(target_os = "linux")]
914 {
915 self.inner.dims()
916 }
917 }
918}
919
920#[cfg(target_os = "linux")]
926const LEVERAGE_CHUNKS_PER_DEVICE: usize = 4;
927
928#[inline]
945#[must_use]
946pub fn try_fast_spectral_leverage_diagonal(
947 x: &gam_linalg::matrix::DesignMatrix,
948 g: ArrayView2<'_, f64>,
949) -> Option<Array1<f64>> {
950 let n = x.nrows();
951 let p = x.ncols();
952 let rank = g.ncols();
953 if g.nrows() != p {
954 invalid_gpu_request(
955 "spectral leverage diagonal",
956 "the design width and spectral-factor height differ",
957 );
958 }
959 if n == 0 || p == 0 || rank == 0 {
960 return decline_gpu(
961 "spectral leverage diagonal",
962 "the workload has an empty dimension",
963 );
964 }
965 #[cfg(not(target_os = "linux"))]
966 {
967 return decline_gpu(
968 "spectral leverage diagonal",
969 "the CUDA backend is not compiled on this platform",
970 );
971 }
972 #[cfg(target_os = "linux")]
973 {
974 let runtime = route_through_gpu(DispatchOp::XtDiagX { n, p })?;
977 let device_count = runtime.device_count().max(1);
978 let byte_chunk = gam_runtime::resource::byte_balanced_row_chunk(p + rank, n);
979 let target_chunks = device_count
980 .saturating_mul(LEVERAGE_CHUNKS_PER_DEVICE)
981 .max(1);
982 let chunk_rows = byte_chunk.min(n.div_ceil(target_chunks).max(1)).max(1);
983
984 let mut tiles: Vec<(std::ops::Range<usize>, Option<Array1<f64>>)> = Vec::new();
987 let mut start = 0usize;
988 while start < n {
989 let end = (start + chunk_rows).min(n);
990 tiles.push((start..end, None));
991 start = end;
992 }
993
994 complete_gpu_attempt(
995 "spectral leverage diagonal scatter",
996 super::pool::scatter_batched(runtime, &mut tiles, |ordinal, tile| {
997 for (range, slot) in tile.iter_mut() {
998 let rows = x.try_row_chunk(range.clone()).ok()?;
999 let xg =
1000 cuda_backend::gemm_on_ordinal(ordinal, rows.view(), g, false, false)?;
1001 let mut out = Array1::<f64>::zeros(range.end - range.start);
1002 for (local, row) in xg.outer_iter().enumerate() {
1003 out[local] = row.iter().map(|&v| v * v).sum();
1004 }
1005 *slot = Some(out);
1006 }
1007 Some(())
1008 }),
1009 );
1010
1011 let mut h = Array1::<f64>::zeros(n);
1012 for (range, slot) in tiles {
1013 let vals = complete_gpu_attempt("spectral leverage diagonal stitch", slot);
1014 if vals.len() != range.end - range.start {
1015 invalid_gpu_result(
1016 "spectral leverage diagonal stitch",
1017 "a device tile produced an invalid row count",
1018 );
1019 }
1020 h.slice_mut(ndarray::s![range]).assign(&vals);
1021 }
1022 Some(h)
1023 }
1024}
1025
1026#[inline]
1027#[must_use]
1028pub fn try_fast_xt_diag_y(
1029 x: ArrayView2<'_, f64>,
1030 w: ArrayView1<'_, f64>,
1031 y: ArrayView2<'_, f64>,
1032) -> Option<Array2<f64>> {
1033 let (n, px) = x.dim();
1034 let (n_y, q) = y.dim();
1035 if n != n_y || n != w.len() {
1036 invalid_gpu_request("Xᵀ·diag(w)·Y", "the row or weight counts differ");
1037 }
1038 if n == 0 || px == 0 || q == 0 {
1039 return decline_gpu(
1040 "Xᵀ·diag(w)·Y",
1041 "the workload has an empty dimension",
1042 );
1043 }
1044 #[cfg(not(target_os = "linux"))]
1045 {
1046 return decline_gpu(
1047 "Xᵀ·diag(w)·Y",
1048 "the CUDA backend is not compiled on this platform",
1049 );
1050 }
1051 #[cfg(target_os = "linux")]
1052 {
1053 let runtime = route_through_gpu(DispatchOp::XtDiagY { n, px, q })?;
1054 Some(complete_gpu_attempt(
1055 "Xᵀ·diag(w)·Y",
1056 cuda_backend::xt_diag_y(runtime, x, w, y),
1057 ))
1058 }
1059}
1060
1061#[inline]
1062#[must_use]
1063pub fn try_fast_joint_hessian_2x2(
1064 x_a: ArrayView2<'_, f64>,
1065 x_b: ArrayView2<'_, f64>,
1066 w_aa: ArrayView1<'_, f64>,
1067 w_ab: ArrayView1<'_, f64>,
1068 w_bb: ArrayView1<'_, f64>,
1069) -> Option<Array2<f64>> {
1070 let (n, pa) = x_a.dim();
1071 let (n_b, pb) = x_b.dim();
1072 if n != n_b || n != w_aa.len() || n != w_ab.len() || n != w_bb.len() {
1073 invalid_gpu_request("joint 2×2 Hessian", "the row or weight counts differ");
1074 }
1075 if n == 0 || (pa == 0 && pb == 0) {
1076 return decline_gpu(
1077 "joint 2×2 Hessian",
1078 "the workload has an empty dimension",
1079 );
1080 }
1081 #[cfg(not(target_os = "linux"))]
1082 {
1083 return decline_gpu(
1084 "joint 2×2 Hessian",
1085 "the CUDA backend is not compiled on this platform",
1086 );
1087 }
1088 #[cfg(target_os = "linux")]
1089 {
1090 let runtime = route_through_gpu(DispatchOp::JointHessian2x2 { n, pa, pb })?;
1091 Some(complete_gpu_attempt(
1092 "joint 2×2 Hessian",
1093 cuda_backend::joint_hessian_2x2(runtime, x_a, x_b, w_aa, w_ab, w_bb),
1094 ))
1095 }
1096}
1097
1098#[inline]
1099#[must_use]
1100pub fn try_cholesky_lower_inplace(a: &mut Array2<f64>) -> Option<()> {
1101 let p = a.nrows();
1102 if p != a.ncols() {
1103 invalid_gpu_request("Cholesky factorization", "the input matrix is non-square");
1104 }
1105 if p == 0 {
1106 return decline_gpu("Cholesky factorization", "the workload has an empty dimension");
1107 }
1108 #[cfg(not(target_os = "linux"))]
1109 {
1110 return decline_gpu(
1111 "Cholesky factorization",
1112 "the CUDA backend is not compiled on this platform",
1113 );
1114 }
1115 #[cfg(target_os = "linux")]
1116 {
1117 let runtime = route_through_gpu(DispatchOp::Potrf { p, batch: 1 })?;
1118 let lower = complete_gpu_attempt(
1119 "Cholesky factorization",
1120 cuda_backend::cholesky_lower(runtime, a.view()),
1121 );
1122 *a = lower;
1123 Some(())
1124 }
1125}
1126
1127#[inline]
1128#[must_use]
1129pub fn try_cholesky_batched_lower_inplace(matrices: &mut [Array2<f64>]) -> Option<()> {
1130 try_cholesky_batched_lower_inplace_with_policy(matrices, super::global_policy())
1131}
1132
1133#[inline]
1134#[must_use]
1135pub fn try_cholesky_batched_lower_inplace_with_policy(
1136 matrices: &mut [Array2<f64>],
1137 gpu_policy: GpuPolicy,
1138) -> Option<()> {
1139 let first = match matrices.first() {
1140 Some(first) => first,
1141 None => return decline_gpu("batched Cholesky factorization", "the batch is empty"),
1142 };
1143 let p = first.nrows();
1144 if first.ncols() != p || matrices.iter().any(|matrix| matrix.dim() != (p, p)) {
1145 invalid_gpu_request(
1146 "batched Cholesky factorization",
1147 "an input matrix is non-square or has a different shape",
1148 );
1149 }
1150 if p == 0 {
1151 return decline_gpu(
1152 "batched Cholesky factorization",
1153 "the workload has an empty dimension",
1154 );
1155 }
1156 #[cfg(not(target_os = "linux"))]
1157 {
1158 return decline_gpu_with_policy(
1159 "batched Cholesky factorization",
1160 "the CUDA backend is not compiled on this platform",
1161 gpu_policy,
1162 );
1163 }
1164 #[cfg(target_os = "linux")]
1165 {
1166 let batch = matrices.len();
1167 let runtime = route_through_gpu_with_policy(
1168 DispatchOp::SmallDenseBatchedPotrf { p, batch },
1169 gpu_policy,
1170 )
1171 .or_else(|| route_through_gpu_with_policy(DispatchOp::Potrf { p, batch }, gpu_policy))?;
1172 if should_split_batch(batch) {
1173 let split = super::pool::scatter_batched(runtime, matrices, |ordinal, tile| {
1179 cuda_backend::cholesky_batched_lower(ordinal, tile)
1180 });
1181 if split.is_some() {
1182 return Some(());
1183 }
1184 }
1185 Some(complete_gpu_attempt(
1186 "batched Cholesky factorization",
1187 cuda_backend::cholesky_batched_lower(runtime.device.ordinal, matrices),
1188 ))
1189 }
1190}
1191
1192#[inline]
1193#[must_use]
1194pub fn try_solve_lower_triangular_matrix(
1195 lower: ArrayView2<'_, f64>,
1196 rhs: ArrayView2<'_, f64>,
1197) -> Option<Array2<f64>> {
1198 let (m, n) = rhs.dim();
1199 if lower.dim() != (m, m) {
1200 invalid_gpu_request(
1201 "lower-triangular solve",
1202 "the triangular matrix shape does not match the right-hand side",
1203 );
1204 }
1205 if m == 0 || n == 0 {
1206 return decline_gpu(
1207 "lower-triangular solve",
1208 "the workload has an empty dimension",
1209 );
1210 }
1211 #[cfg(not(target_os = "linux"))]
1212 {
1213 return decline_gpu(
1214 "lower-triangular solve",
1215 "the CUDA backend is not compiled on this platform",
1216 );
1217 }
1218 #[cfg(target_os = "linux")]
1219 {
1220 let runtime = route_through_gpu(DispatchOp::Trsm { m, n })?;
1221 Some(complete_gpu_attempt(
1222 "lower-triangular solve",
1223 cuda_backend::trsm(runtime, lower, rhs, false),
1224 ))
1225 }
1226}
1227
1228#[inline]
1229#[must_use]
1230pub fn try_solve_upper_triangular_matrix(
1231 upper: ArrayView2<'_, f64>,
1232 rhs: ArrayView2<'_, f64>,
1233) -> Option<Array2<f64>> {
1234 let (m, n) = rhs.dim();
1235 if upper.dim() != (m, m) {
1236 invalid_gpu_request(
1237 "upper-triangular solve",
1238 "the triangular matrix shape does not match the right-hand side",
1239 );
1240 }
1241 if m == 0 || n == 0 {
1242 return decline_gpu(
1243 "upper-triangular solve",
1244 "the workload has an empty dimension",
1245 );
1246 }
1247 #[cfg(not(target_os = "linux"))]
1248 {
1249 return decline_gpu(
1250 "upper-triangular solve",
1251 "the CUDA backend is not compiled on this platform",
1252 );
1253 }
1254 #[cfg(target_os = "linux")]
1255 {
1256 let runtime = route_through_gpu(DispatchOp::Trsm { m, n })?;
1257 Some(complete_gpu_attempt(
1258 "upper-triangular solve",
1259 cuda_backend::trsm(runtime, upper, rhs, true),
1260 ))
1261 }
1262}
1263
1264#[cfg(test)]
1265mod pre_probe_gate_tests {
1266 use super::{DispatchOp, GpuDispatchPolicy};
1273
1274 #[test]
1275 fn admissibility_bound_never_tightens_the_real_admission() {
1276 let floor_policy = GpuDispatchPolicy {
1282 gemm_min_flops: usize::try_from(GpuDispatchPolicy::MIN_CALIBRATABLE_GEMM_FLOPS)
1283 .expect("fits usize"),
1284 potrf_min_p: GpuDispatchPolicy::MIN_CALIBRATABLE_POTRF_P,
1285 xtwx_flops_min: 4_194_304, ..GpuDispatchPolicy::default()
1287 };
1288 let policies = [GpuDispatchPolicy::default(), floor_policy];
1289 let ops = [
1290 DispatchOp::Gemm {
1291 m: 64,
1292 n: 64,
1293 k: 64,
1294 },
1295 DispatchOp::Gemm {
1296 m: 63,
1297 n: 64,
1298 k: 64,
1299 },
1300 DispatchOp::BatchedGemm {
1301 batch: 8,
1302 m: 64,
1303 n: 64,
1304 k: 8,
1305 },
1306 DispatchOp::Gemv { m: 512, k: 512 },
1307 DispatchOp::Potrf { p: 64, batch: 1 },
1308 DispatchOp::Potrf { p: 63, batch: 1 },
1309 DispatchOp::Potrf { p: 24, batch: 512 },
1310 DispatchOp::SmallDenseBatchedPotrf { p: 24, batch: 8 },
1311 DispatchOp::SmallDenseBatchedPotrf { p: 24, batch: 7 },
1312 DispatchOp::Trsm { m: 128, n: 64 },
1313 DispatchOp::XtDiagX { n: 50_000, p: 96 },
1314 DispatchOp::XtDiagX { n: 700, p: 24 },
1315 DispatchOp::XtDiagY {
1316 n: 50_000,
1317 px: 96,
1318 q: 8,
1319 },
1320 DispatchOp::JointHessian2x2 {
1321 n: 50_000,
1322 pa: 64,
1323 pb: 64,
1324 },
1325 ];
1326 for policy in &policies {
1327 for op in ops {
1328 let admitted = match op {
1329 DispatchOp::Gemm { m, n, k } => {
1330 op.flops() >= policy.gemm_min_flops as u128 && m.min(n).min(k) > 0
1331 }
1332 DispatchOp::BatchedGemm { batch, m, n, k } => {
1333 op.flops() >= policy.gemm_min_flops as u128
1334 && batch > 1
1335 && m.min(n).min(k) > 0
1336 }
1337 DispatchOp::Gemv { m, k } => {
1338 op.flops() >= policy.gemm_min_flops as u128 && m > 0 && k > 0
1339 }
1340 DispatchOp::Potrf { p, batch } => {
1341 p > 0
1342 && batch > 0
1343 && (p >= policy.potrf_min_p
1344 || (batch > 1 && op.flops() >= policy.gemm_min_flops as u128))
1345 }
1346 DispatchOp::SmallDenseBatchedPotrf { p, batch } => {
1347 p > 0
1348 && p <= policy.small_dense_batched_potrf_max_p
1349 && batch >= policy.small_dense_batched_potrf_min_batch
1350 }
1351 DispatchOp::Trsm { m, n } => {
1352 op.flops() >= policy.gemm_min_flops as u128 && m > 0 && n > 0
1353 }
1354 DispatchOp::XtDiagX { n, p } => policy.xtwx_target_is_gpu(n, p, true),
1355 DispatchOp::XtDiagY { n, px, q } => policy.xtwy_target_is_gpu(n, px, q, true),
1356 DispatchOp::JointHessian2x2 { n, pa, pb } => {
1357 n > 0
1358 && (pa > 0 || pb > 0)
1359 && op.flops() >= policy.gemm_min_flops as u128
1360 }
1361 };
1362 if admitted {
1363 assert!(
1364 op.admissible_under_any_policy(),
1365 "pre-probe bound must not refuse an op the real admission accepts: \
1366 {op:?} under {policy:?}"
1367 );
1368 }
1369 }
1370 }
1371 }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376 use super::{DispatchOp, route_through_gpu, try_fast_ab};
1377 use crate::GpuPolicy;
1378 use crate::device_runtime::GpuRuntime;
1379
1380 fn available_runtime(label: &str) -> Option<&'static GpuRuntime> {
1381 match GpuRuntime::resolve(GpuPolicy::Auto) {
1382 Ok(runtime) => runtime,
1383 Err(error) => panic!("[{label}] GPU probe fault: {error}"),
1384 }
1385 }
1386
1387 #[test]
1388 fn sae_shape_dispatch_ops_decline_without_cuda_else_route_when_cuda_runtime_is_present() {
1389 let n = 2_000usize;
1390 let p = 2_048usize;
1391 let m = 12usize;
1392 let k = 8usize;
1393 let dense_reduction_ops = [
1394 DispatchOp::XtDiagX { n, p },
1395 DispatchOp::XtDiagY { n, px: p, q: m * k },
1396 DispatchOp::JointHessian2x2 {
1397 n,
1398 pa: p,
1399 pb: m * k,
1400 },
1401 DispatchOp::Gemm {
1402 m: p,
1403 n: p,
1404 k: n * m,
1405 },
1406 ];
1407 let batched_potrf = DispatchOp::SmallDenseBatchedPotrf { p: m, batch: n };
1408 let Some(runtime) = available_runtime("sae dispatch gate") else {
1409 for op in dense_reduction_ops
1410 .iter()
1411 .copied()
1412 .chain(std::iter::once(batched_potrf))
1413 {
1414 assert!(
1415 route_through_gpu(op).is_none(),
1416 "no CUDA runtime is available, yet the SAE dispatch gate admitted {op:?}"
1417 );
1418 }
1419 return;
1420 };
1421
1422 for op in dense_reduction_ops {
1423 assert!(
1424 op.flops() >= runtime.policy.gemm_min_flops as u128,
1425 "SAE dispatch fixture must clear the runtime GEMM work floor: op={op:?}, flops={}, floor={}",
1426 op.flops(),
1427 runtime.policy.gemm_min_flops
1428 );
1429 assert!(
1430 route_through_gpu(op).is_some(),
1431 "SAE dispatch fixture should route to GPU when CUDA is present: {op:?}"
1432 );
1433 }
1434
1435 assert!(
1436 route_through_gpu(batched_potrf).is_some(),
1437 "uniform SAE row blocks should reach the small-dense batched POTRF gate"
1438 );
1439 }
1440
1441 #[test]
1448 fn global_runtime_declines_without_cuda_else_installs_fast_ab_hook_and_matches_cpu() {
1449 use ndarray::Array2;
1450
1451 let (m, k, n) = (512usize, 512usize, 512usize);
1452 let Some(_runtime) = available_runtime("fast_ab hook") else {
1453 assert!(
1454 route_through_gpu(DispatchOp::Gemm { m, n, k }).is_none(),
1455 "no CUDA runtime is available, yet a profitable dense GEMM was admitted"
1456 );
1457 return;
1458 };
1459 assert!(
1461 gam_linalg::gpu_hook::gpu_dispatch().is_some(),
1462 "GpuRuntime::resolve(Auto) returned a device but did not register the \
1463 dense-GEMM dispatch hook — fast_ab would silently stay on the CPU"
1464 );
1465
1466 assert!(
1470 route_through_gpu(DispatchOp::Gemm { m, n, k }).is_some(),
1471 "a 268 MFLOP GEMM must clear the policy floor and route to GPU"
1472 );
1473
1474 let a = Array2::<f64>::from_shape_fn((m, k), |(i, j)| {
1476 ((i * 7 + j * 3) % 13) as f64 * 0.01 - 0.06
1477 });
1478 let b = Array2::<f64>::from_shape_fn((k, n), |(i, j)| {
1479 ((i * 5 + j * 11) % 17) as f64 * 0.01 - 0.08
1480 });
1481
1482 let gpu = try_fast_ab(a.view(), b.view())
1484 .expect("profitable GEMM must produce a device result once admitted");
1485
1486 let mut cpu = Array2::<f64>::zeros((m, n));
1488 for i in 0..m {
1489 for j in 0..n {
1490 let mut acc = 0.0f64;
1491 for p in 0..k {
1492 acc += a[[i, p]] * b[[p, j]];
1493 }
1494 cpu[[i, j]] = acc;
1495 }
1496 }
1497
1498 let mut max_abs = 0.0f64;
1499 for i in 0..m {
1500 for j in 0..n {
1501 max_abs = max_abs.max((gpu[[i, j]] - cpu[[i, j]]).abs());
1502 }
1503 }
1504 assert!(
1505 max_abs < 1e-9,
1506 "device GEMM disagreed with the CPU oracle: max|Δ| = {max_abs:e}"
1507 );
1508 }
1509
1510 #[cfg(target_os = "linux")]
1517 #[test]
1518 fn transpose_free_gemm_declines_without_cuda_else_matches_cpu_all_trans_and_shapes() {
1519 use crate::blas::gemm_cuda;
1520 use ndarray::Array2;
1521
1522 let Some(runtime) = available_runtime("gemm transpose-free") else {
1523 assert!(
1524 route_through_gpu(DispatchOp::Gemm {
1525 m: 512,
1526 n: 512,
1527 k: 512,
1528 })
1529 .is_none(),
1530 "no CUDA runtime is available, yet the transpose-free GEMM seam admitted work"
1531 );
1532 return;
1533 };
1534
1535 let cases = [(6usize, 4usize, 5usize), (17, 23, 9), (200, 31, 7)];
1538 for (m, k, n) in cases {
1539 let mk = Array2::<f64>::from_shape_fn((m, k), |(i, j)| {
1543 ((i * 31 + j * 17) % 19) as f64 * 0.013 - 0.11
1544 });
1545 let km = Array2::<f64>::from_shape_fn((k, m), |(i, j)| {
1546 ((i * 13 + j * 29) % 23) as f64 * 0.011 - 0.07
1547 });
1548 let kn = Array2::<f64>::from_shape_fn((k, n), |(i, j)| {
1549 ((i * 7 + j * 5) % 17) as f64 * 0.017 - 0.09
1550 });
1551 let nk = Array2::<f64>::from_shape_fn((n, k), |(i, j)| {
1552 ((i * 19 + j * 11) % 13) as f64 * 0.015 - 0.05
1553 });
1554
1555 for &trans_a in &[false, true] {
1556 for &trans_b in &[false, true] {
1557 let a = if trans_a { &km } else { &mk };
1558 let b = if trans_b { &nk } else { &kn };
1559
1560 let gpu = gemm_cuda(runtime, a.view(), b.view(), trans_a, trans_b).expect(
1561 "transpose-free device GEMM must produce a result when a device is present",
1562 );
1563 assert_eq!(
1564 gpu.dim(),
1565 (m, n),
1566 "output shape wrong for trans_a={trans_a} trans_b={trans_b} ({m}×{k}×{n})"
1567 );
1568
1569 let mut cpu = Array2::<f64>::zeros((m, n));
1571 for i in 0..m {
1572 for j in 0..n {
1573 let mut acc = 0.0f64;
1574 for p in 0..k {
1575 let av = if trans_a { a[[p, i]] } else { a[[i, p]] };
1576 let bv = if trans_b { b[[j, p]] } else { b[[p, j]] };
1577 acc += av * bv;
1578 }
1579 cpu[[i, j]] = acc;
1580 }
1581 }
1582
1583 let mut max_abs = 0.0f64;
1584 for i in 0..m {
1585 for j in 0..n {
1586 max_abs = max_abs.max((gpu[[i, j]] - cpu[[i, j]]).abs());
1587 }
1588 }
1589 assert!(
1590 max_abs < 1e-9,
1591 "transpose-free GEMM mismatch (trans_a={trans_a} trans_b={trans_b}, \
1592 {m}×{k}×{n}): max|Δ| = {max_abs:e}"
1593 );
1594 }
1595 }
1596 }
1597 }
1598}
1599
1600#[cfg(target_os = "linux")]
1606mod cuda_backend {
1607 use ndarray::{Array1, Array2, Array3, ArrayView1, ArrayView2, ArrayView3};
1618
1619 use super::super::device_runtime::GpuRuntime;
1620 use crate::driver::{from_col_major, to_col_major, to_i32};
1621 use cudarc::cusolver::{DnHandle, sys as cusolver_sys};
1622 use cudarc::driver::{DevicePtrMut, sys as driver_sys};
1623
1624 #[inline]
1625 pub(super) fn gemm(
1626 runtime: &GpuRuntime,
1627 a: ArrayView2<'_, f64>,
1628 b: ArrayView2<'_, f64>,
1629 trans_a: bool,
1630 trans_b: bool,
1631 ) -> Option<Array2<f64>> {
1632 super::super::blas::gemm_cuda(runtime, a, b, trans_a, trans_b)
1633 }
1634
1635 #[inline]
1636 pub(super) fn gemm_on_ordinal(
1637 ordinal: usize,
1638 a: ArrayView2<'_, f64>,
1639 b: ArrayView2<'_, f64>,
1640 trans_a: bool,
1641 trans_b: bool,
1642 ) -> Option<Array2<f64>> {
1643 super::super::blas::gemm_on_ordinal_cuda(ordinal, a, b, trans_a, trans_b)
1644 }
1645
1646 #[inline]
1647 pub(super) fn gemv(
1648 runtime: &GpuRuntime,
1649 a: ArrayView2<'_, f64>,
1650 v: ArrayView1<'_, f64>,
1651 trans_a: bool,
1652 ) -> Option<Array1<f64>> {
1653 super::super::blas::gemv_cuda(runtime, a, v, trans_a)
1654 }
1655
1656 #[inline]
1657 pub(super) fn gemm_broadcast_b_batched(
1658 ordinal: usize,
1659 a: ArrayView3<'_, f64>,
1660 b: ArrayView2<'_, f64>,
1661 ) -> Option<Array3<f64>> {
1662 super::super::blas::gemm_broadcast_b_batched_cuda(ordinal, a, b)
1663 }
1664
1665 #[inline]
1666 pub(super) fn gemm_abt_strided_batched(
1667 ordinal: usize,
1668 a: ArrayView3<'_, f64>,
1669 b: ArrayView3<'_, f64>,
1670 ) -> Option<Array3<f64>> {
1671 super::super::blas::gemm_abt_strided_batched_cuda(ordinal, a, b)
1672 }
1673
1674 #[inline]
1675 pub(super) fn xt_diag_x(
1676 runtime: &GpuRuntime,
1677 x: ArrayView2<'_, f64>,
1678 w: ArrayView1<'_, f64>,
1679 ) -> Option<Array2<f64>> {
1680 super::super::blas::xt_diag_x_cuda(runtime, x, w)
1681 }
1682
1683 #[inline]
1684 pub(super) fn xt_diag_y(
1685 runtime: &GpuRuntime,
1686 x: ArrayView2<'_, f64>,
1687 w: ArrayView1<'_, f64>,
1688 y: ArrayView2<'_, f64>,
1689 ) -> Option<Array2<f64>> {
1690 super::super::blas::xt_diag_y_cuda(runtime, x, w, y)
1691 }
1692
1693 #[inline]
1694 pub(super) fn joint_hessian_2x2(
1695 runtime: &GpuRuntime,
1696 x_a: ArrayView2<'_, f64>,
1697 x_b: ArrayView2<'_, f64>,
1698 w_aa: ArrayView1<'_, f64>,
1699 w_ab: ArrayView1<'_, f64>,
1700 w_bb: ArrayView1<'_, f64>,
1701 ) -> Option<Array2<f64>> {
1702 super::super::blas::joint_hessian_2x2_cuda(runtime, x_a, x_b, w_aa, w_ab, w_bb)
1703 }
1704
1705 #[inline]
1706 pub(super) fn trsm(
1707 runtime: &GpuRuntime,
1708 triangular: ArrayView2<'_, f64>,
1709 rhs: ArrayView2<'_, f64>,
1710 upper: bool,
1711 ) -> Option<Array2<f64>> {
1712 super::super::blas::trsm_cuda(runtime, triangular, rhs, upper)
1713 }
1714
1715 #[inline]
1716 pub(super) fn cholesky_lower(
1717 runtime: &GpuRuntime,
1718 a: ArrayView2<'_, f64>,
1719 ) -> Option<Array2<f64>> {
1720 let (p, p2) = a.dim();
1721 if p == 0 || p != p2 {
1722 return None;
1723 }
1724 let stream = super::super::device_runtime::cuda_context_for(runtime.device.ordinal)?
1725 .new_stream()
1726 .ok()?;
1727 let solver = DnHandle::new(stream.clone()).ok()?;
1728 let a_col = to_col_major(&a);
1729 let mut a_dev = stream.clone_htod(&*a_col).ok()?;
1730 potrf_lower_in_place(&solver, &stream, p, &mut a_dev)?;
1731 let factor_col = stream.clone_dtoh(&a_dev).ok()?;
1732 let mut lower = from_col_major(&factor_col, p, p)?;
1733 for row in 0..p {
1734 for col in (row + 1)..p {
1735 lower[[row, col]] = 0.0;
1736 }
1737 }
1738 Some(lower)
1739 }
1740
1741 #[inline]
1745 pub(super) fn cholesky_batched_lower(
1746 ordinal: usize,
1747 matrices: &mut [Array2<f64>],
1748 ) -> Option<()> {
1749 let first = matrices.first()?;
1750 let p = first.nrows();
1751 if p == 0 || first.ncols() != p || matrices.iter().any(|matrix| matrix.dim() != (p, p)) {
1752 return None;
1753 }
1754
1755 let stream = super::super::device_runtime::cuda_context_for(ordinal)?
1756 .new_stream()
1757 .ok()?;
1758 let solver = DnHandle::new(stream.clone()).ok()?;
1759 let matrix_len = p.checked_mul(p)?;
1760 let mut batch_col = Vec::with_capacity(matrices.len().checked_mul(matrix_len)?);
1761 for matrix in matrices.iter() {
1762 batch_col.extend(to_col_major(&matrix.view()).iter().copied());
1763 }
1764 let mut matrices_dev = stream.clone_htod(&batch_col).ok()?;
1765 let matrix_ptrs = {
1766 let (base_ptr, _matrix_record) = matrices_dev.device_ptr_mut(&stream);
1767 let bytes_per_matrix = driver_sys::CUdeviceptr::try_from(
1768 matrix_len.checked_mul(std::mem::size_of::<f64>())?,
1769 )
1770 .ok()?;
1771 let mut matrix_ptrs = Vec::with_capacity(matrices.len());
1772 for idx in 0..matrices.len() {
1773 let offset = driver_sys::CUdeviceptr::try_from(idx).ok()? * bytes_per_matrix;
1774 matrix_ptrs.push(base_ptr + offset);
1775 }
1776 matrix_ptrs
1777 };
1778 let mut matrix_ptrs_dev = stream.clone_htod(&matrix_ptrs).ok()?;
1779 let mut info_dev = stream.alloc_zeros::<i32>(matrices.len()).ok()?;
1780 let p_i = to_i32(p)?;
1781 let batch_i = to_i32(matrices.len())?;
1782 {
1783 let (ptrs_ptr, _ptrs_record) = matrix_ptrs_dev.device_ptr_mut(&stream);
1784 let (info_ptr, _info_record) = info_dev.device_ptr_mut(&stream);
1785 let status = unsafe {
1789 cusolver_sys::cusolverDnDpotrfBatched(
1790 solver.cu(),
1791 cusolver_sys::cublasFillMode_t::CUBLAS_FILL_MODE_LOWER,
1792 p_i,
1793 ptrs_ptr as *mut *mut f64,
1794 p_i,
1795 info_ptr as *mut i32,
1796 batch_i,
1797 )
1798 };
1799 check_cusolver(status)?;
1800 }
1801 let info_host = stream.clone_dtoh(&info_dev).ok()?;
1802 if info_host.iter().any(|info| *info != 0) {
1803 return None;
1804 }
1805 let factored_col = stream.clone_dtoh(&matrices_dev).ok()?;
1806 for (idx, matrix) in matrices.iter_mut().enumerate() {
1807 let start = idx.checked_mul(matrix_len)?;
1808 let end = start.checked_add(matrix_len)?;
1809 let mut lower = from_col_major(&factored_col[start..end], p, p)?;
1810 for row in 0..p {
1811 for col in (row + 1)..p {
1812 lower[[row, col]] = 0.0;
1813 }
1814 }
1815 *matrix = lower;
1816 }
1817 Some(())
1818 }
1819
1820 fn potrf_lower_in_place(
1826 solver: &DnHandle,
1827 stream: &std::sync::Arc<cudarc::driver::CudaStream>,
1828 p: usize,
1829 a: &mut cudarc::driver::CudaSlice<f64>,
1830 ) -> Option<()> {
1831 crate::solver::potrf_in_place_generic::<f64>(solver, stream, p, a).ok()
1832 }
1833
1834 #[inline]
1835 fn check_cusolver(status: cusolver_sys::cusolverStatus_t) -> Option<()> {
1836 if status == cusolver_sys::cusolverStatus_t::CUSOLVER_STATUS_SUCCESS {
1837 Some(())
1838 } else {
1839 None
1840 }
1841 }
1842}