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