1#[macro_use]
18pub mod gpu_error;
19pub mod backend_probe;
20pub mod blas;
21#[cfg(target_os = "linux")]
22pub mod calibration;
23pub mod device;
24pub mod device_cache;
25pub mod device_runtime;
26pub mod dictionary_score;
27pub mod driver;
28pub mod encode_throughput;
29pub mod linalg_dispatch;
30pub mod memory;
31pub mod numerics_device;
32pub mod numerics_host;
33pub mod policy;
34pub mod pool;
35pub mod profile;
36pub mod solver;
37pub mod test_gate;
40
41pub mod kernels;
43
44pub use device::GpuDeviceInfo;
45pub use device_runtime::{GpuAbsence, GpuAvailability, GpuAvailabilityRef, GpuRuntime};
46pub use dictionary_score::{
47 DEFAULT_DICTIONARY_SCORE_MIN_ELEMS, DEFAULT_DICTIONARY_SCORE_TILE_ELEMS,
48 DictionaryScoreRoutePlan,
49};
50pub use gpu_error::GpuError;
51pub use memory::{DeviceBuffer, DeviceCsrMatrix, DeviceMatrix, DeviceVector};
52pub use policy::{GpuDispatchPolicy, GpuMixedPrecisionPolicy};
53pub use pool::{balanced_partition, scatter_batched};
54pub use profile::{GpuExecutionTelemetry, KernelStat, KernelStatsSnapshot};
55
56use serde::{Deserialize, Serialize};
69use std::fmt;
70use std::sync::OnceLock;
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub enum CudaBackendStatus {
74 CudaUnavailable,
75 CudaReady,
76}
77
78#[inline]
79pub(crate) fn cuda_backend_status() -> Result<CudaBackendStatus, GpuError> {
80 Ok(if device_runtime::GpuRuntime::resolve(global_policy())?.is_some() {
81 CudaBackendStatus::CudaReady
82 } else {
83 CudaBackendStatus::CudaUnavailable
84 })
85}
86
87#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub enum GpuPolicy {
91 #[default]
93 Auto,
94 Off,
96 Required,
98}
99
100impl GpuPolicy {
101 pub fn parse(raw: &str) -> Option<Self> {
102 match raw.trim().to_ascii_lowercase().as_str() {
103 "auto" => Some(Self::Auto),
104 "off" => Some(Self::Off),
105 "required" => Some(Self::Required),
106 _ => None,
107 }
108 }
109
110 #[inline]
111 pub const fn as_str(self) -> &'static str {
112 match self {
113 Self::Auto => "auto",
114 Self::Off => "off",
115 Self::Required => "required",
116 }
117 }
118}
119
120impl fmt::Display for GpuPolicy {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 f.write_str(self.as_str())
123 }
124}
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub enum GpuKernel {
128 DenseMatvec,
129 DenseTransposeMatvec,
130 DenseXtWX,
131 CandidateScreen,
132 DenseSolve,
133 MatrixFreePcg,
134 SparseAssembly,
135 SpatialKernelOperator,
136 MarginalSlopeRows,
137 RemlTrace,
138 FinalInference,
139}
140
141impl GpuKernel {
142 pub const fn as_str(self) -> &'static str {
143 match self {
144 Self::DenseMatvec => "dense-matvec",
145 Self::DenseTransposeMatvec => "dense-transpose-matvec",
146 Self::DenseXtWX => "dense-xtwx",
147 Self::CandidateScreen => "candidate-screen",
148 Self::DenseSolve => "dense-solve",
149 Self::MatrixFreePcg => "matrix-free-pcg",
150 Self::SparseAssembly => "sparse-assembly",
151 Self::SpatialKernelOperator => "spatial-kernel-operator",
152 Self::MarginalSlopeRows => "marginal-slope-rows",
153 Self::RemlTrace => "reml-trace",
154 Self::FinalInference => "final-inference",
155 }
156 }
157}
158
159#[derive(Clone, Debug)]
161pub struct GpuDecision {
162 pub policy: GpuPolicy,
163 pub kernel: GpuKernel,
164 pub use_gpu: bool,
165 pub reason: &'static str,
166}
167
168static POLICY: OnceLock<GpuPolicy> = OnceLock::new();
169
170#[inline]
171pub fn global_policy() -> GpuPolicy {
172 match POLICY.get() {
179 Some(p) => *p,
180 None => GpuPolicy::Auto,
181 }
182}
183
184pub fn configure_global_policy(policy: GpuPolicy) {
191 if let Err(rejected) = POLICY.set(policy) {
194 log::debug!(
195 "gam-gpu: global policy already configured as {:?}; ignoring the later {rejected:?}",
196 POLICY.get()
197 );
198 }
199}
200
201#[inline]
208pub fn cuda_selected() -> Result<bool, GpuError> {
209 match global_policy() {
210 GpuPolicy::Off => Ok(false),
211 policy @ (GpuPolicy::Auto | GpuPolicy::Required) => {
212 Ok(device_runtime::GpuRuntime::resolve(policy)?.is_some())
213 }
214 }
215}
216
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
225pub enum GpuEligibility {
226 BackendNotCompiled,
228 WorkloadBelowThreshold,
231 Eligible,
234}
235
236impl GpuEligibility {
237 #[inline]
241 pub const fn from_flags(supported: bool, large_enough: bool) -> Self {
242 if !supported {
243 Self::BackendNotCompiled
244 } else if !large_enough {
245 Self::WorkloadBelowThreshold
246 } else {
247 Self::Eligible
248 }
249 }
250}
251
252pub fn decide(
256 kernel: GpuKernel,
257 eligibility: GpuEligibility,
258) -> Result<GpuDecision, GpuError> {
259 let policy = global_policy();
260 let runtime_available = device_runtime::GpuRuntime::resolve(policy)?.is_some();
266 let (use_gpu, reason) = match (policy, eligibility) {
267 (GpuPolicy::Off, _) => (false, "cpu-gpu-policy-off"),
268 (GpuPolicy::Auto, GpuEligibility::BackendNotCompiled) => {
269 (false, "cpu-gpu-backend-not-compiled")
270 }
271 (GpuPolicy::Auto, _) if !runtime_available => (false, "cpu-gpu-runtime-unavailable"),
272 (GpuPolicy::Auto, GpuEligibility::WorkloadBelowThreshold) => {
273 (false, "cpu-workload-below-gpu-threshold")
274 }
275 (GpuPolicy::Auto, GpuEligibility::Eligible) => (true, "gpu-auto-supported"),
276 (GpuPolicy::Required, GpuEligibility::BackendNotCompiled) => {
277 (false, "cpu-gpu-required-unsupported")
278 }
279 (GpuPolicy::Required, GpuEligibility::WorkloadBelowThreshold)
282 | (GpuPolicy::Required, GpuEligibility::Eligible) => (true, "gpu-required-supported"),
283 };
284 Ok(GpuDecision {
285 policy,
286 kernel,
287 use_gpu,
288 reason,
289 })
290}
291
292impl GpuDecision {
293 pub fn require_supported(&self) -> Result<(), String> {
294 if self.policy == GpuPolicy::Required && !self.use_gpu {
295 return Err(format!(
296 "gpu=required requested kernel '{}' but no supported device backend is available ({})",
297 self.kernel.as_str(),
298 self.reason
299 ));
300 }
301 Ok(())
302 }
303
304 pub fn log(self) {
305 log::debug!(
306 "[GPU backend] kernel={} policy={} selected={} reason={}",
307 self.kernel.as_str(),
308 self.policy.as_str(),
309 self.use_gpu,
310 self.reason
311 );
312 }
313}
314
315pub fn log_backend_inventory_once() {
319 static LOGGED: OnceLock<()> = OnceLock::new();
320 LOGGED.get_or_init(|| {
321 let compiled_backends = if cfg!(target_os = "linux") {
322 "cuda-dynamic"
323 } else {
324 "none"
325 };
326 log::debug!(
327 "[GPU backend] policy={} compiled_backends={} kernels=dense-matvec,dense-transpose-matvec,dense-xtwx,candidate-screen,dense-solve,matrix-free-pcg,sparse-assembly,spatial-kernel-operator,marginal-slope-rows,reml-trace,final-inference",
328 global_policy().as_str(),
329 compiled_backends
330 );
331 });
332}
333
334#[inline]
335pub fn try_fast_ab(
336 a: ndarray::ArrayView2<'_, f64>,
337 b: ndarray::ArrayView2<'_, f64>,
338) -> Option<ndarray::Array2<f64>> {
339 linalg_dispatch::try_fast_ab(a, b)
340}
341#[inline]
342pub fn try_fast_atb_on_ordinal(
343 ordinal: usize,
344 a: ndarray::ArrayView2<'_, f64>,
345 b: ndarray::ArrayView2<'_, f64>,
346) -> Option<ndarray::Array2<f64>> {
347 linalg_dispatch::try_fast_atb_on_ordinal(ordinal, a, b)
348}
349#[inline]
350pub fn try_fast_av(
351 a: ndarray::ArrayView2<'_, f64>,
352 v: ndarray::ArrayView1<'_, f64>,
353) -> Option<ndarray::Array1<f64>> {
354 linalg_dispatch::try_fast_av(a, v)
355}
356#[inline]
357pub fn try_fast_atv(
358 a: ndarray::ArrayView2<'_, f64>,
359 v: ndarray::ArrayView1<'_, f64>,
360) -> Option<ndarray::Array1<f64>> {
361 linalg_dispatch::try_fast_atv(a, v)
362}
363#[inline]
364pub fn try_fast_ab_broadcast_b_batched(
365 a: ndarray::ArrayView3<'_, f64>,
366 b: ndarray::ArrayView2<'_, f64>,
367) -> Option<ndarray::Array3<f64>> {
368 linalg_dispatch::try_fast_ab_broadcast_b_batched(a, b)
369}
370#[inline]
371pub fn try_fast_abt_strided_batched(
372 a: ndarray::ArrayView3<'_, f64>,
373 b: ndarray::ArrayView3<'_, f64>,
374) -> Option<ndarray::Array3<f64>> {
375 linalg_dispatch::try_fast_abt_strided_batched(a, b)
376}
377#[inline]
378pub fn try_fast_abt_strided_batched_with_policy(
379 a: ndarray::ArrayView3<'_, f64>,
380 b: ndarray::ArrayView3<'_, f64>,
381 policy: GpuPolicy,
382) -> Option<ndarray::Array3<f64>> {
383 linalg_dispatch::try_fast_abt_strided_batched_with_policy(a, b, policy)
384}
385#[inline]
386pub fn try_cholesky_lower_inplace(a: &mut ndarray::Array2<f64>) -> Option<()> {
387 linalg_dispatch::try_cholesky_lower_inplace(a)
388}
389#[inline]
390pub fn try_cholesky_batched_lower_inplace(matrices: &mut [ndarray::Array2<f64>]) -> Option<()> {
391 linalg_dispatch::try_cholesky_batched_lower_inplace(matrices)
392}
393#[inline]
394pub fn try_cholesky_batched_lower_inplace_with_policy(
395 matrices: &mut [ndarray::Array2<f64>],
396 policy: GpuPolicy,
397) -> Option<()> {
398 linalg_dispatch::try_cholesky_batched_lower_inplace_with_policy(matrices, policy)
399}
400#[inline]
401pub fn try_solve_lower_triangular_matrix(
402 lower: ndarray::ArrayView2<'_, f64>,
403 rhs: ndarray::ArrayView2<'_, f64>,
404) -> Option<ndarray::Array2<f64>> {
405 linalg_dispatch::try_solve_lower_triangular_matrix(lower, rhs)
406}
407#[inline]
408pub fn try_solve_upper_triangular_matrix(
409 upper: ndarray::ArrayView2<'_, f64>,
410 rhs: ndarray::ArrayView2<'_, f64>,
411) -> Option<ndarray::Array2<f64>> {
412 linalg_dispatch::try_solve_upper_triangular_matrix(upper, rhs)
413}
414#[cfg(test)]
415mod policy_tests {
416 use super::*;
417
418 #[test]
419 fn parses_canonical_user_gpu_policy_values() {
420 assert_eq!(GpuPolicy::parse("auto"), Some(GpuPolicy::Auto));
421 assert_eq!(GpuPolicy::parse("off"), Some(GpuPolicy::Off));
422 assert_eq!(
423 GpuPolicy::parse("required"),
424 Some(GpuPolicy::Required)
425 );
426 assert_eq!(GpuPolicy::parse("force"), None);
427 assert_eq!(GpuPolicy::parse("cpu"), None);
428 assert_eq!(GpuPolicy::parse(""), None);
429 assert_eq!(GpuPolicy::parse("wat"), None);
430 }
431
432 #[test]
433 fn execution_path_defaults_to_cpu() {
434 use gam_problem::ExecutionPath;
435 assert_eq!(ExecutionPath::default(), ExecutionPath::Cpu);
440 assert!(!ExecutionPath::Cpu.used_device());
441 assert!(ExecutionPath::GpuResidentFull.used_device());
442 }
443
444 #[test]
445 fn gpu_mode_required_fails_closed_when_device_absent() {
446 use crate::device_runtime::{GpuAvailabilityRef, GpuRuntime};
447 assert!(GpuRuntime::resolve(GpuPolicy::Off).unwrap().is_none());
449
450 match GpuRuntime::availability() {
451 Ok(GpuAvailabilityRef::Available(_)) => {
452 assert!(matches!(
454 GpuRuntime::resolve(GpuPolicy::Required),
455 Ok(Some(_))
456 ));
457 assert!(matches!(GpuRuntime::resolve(GpuPolicy::Auto), Ok(Some(_))));
458 }
459 Ok(GpuAvailabilityRef::Absent(_)) => {
460 let required = GpuRuntime::resolve(GpuPolicy::Required);
463 assert!(
464 matches!(required, Err(GpuError::RequiredDeviceUnavailable { .. })),
465 "GpuPolicy::Required must fail closed when the device is absent, got {required:?}"
466 );
467 assert!(matches!(GpuRuntime::resolve(GpuPolicy::Auto), Ok(None)));
468 }
469 Err(error) => panic!("GPU probe fault must fail this contract test: {error}"),
470 }
471 }
472
473 #[test]
474 fn pirls_loop_admission_requires_runtime_size_and_known_family() {
475 use crate::policy::{PirlsLoopAdmission, PirlsLoopCurvatureKind, PirlsLoopFamilyKind};
476 let pol = GpuDispatchPolicy::default();
477 let base = PirlsLoopAdmission {
478 n: 80_000,
479 p: 44,
480 family: Some(PirlsLoopFamilyKind::BernoulliLogit),
481 curvature: PirlsLoopCurvatureKind::Fisher,
482 gpu_available: true,
483 };
484 assert!(pol.should_use_gpu_pirls_loop(base));
485 assert!(!pol.should_use_gpu_pirls_loop(PirlsLoopAdmission {
487 gpu_available: false,
488 ..base
489 }));
490 assert!(!pol.should_use_gpu_pirls_loop(PirlsLoopAdmission { n: 1_000, ..base }));
492 assert!(pol.should_use_gpu_pirls_loop(PirlsLoopAdmission {
494 n: 2_000,
495 p: 2_048,
496 ..base
497 }));
498 assert!(!pol.should_use_gpu_pirls_loop(PirlsLoopAdmission { p: 8, ..base }));
500 assert!(!pol.should_use_gpu_pirls_loop(PirlsLoopAdmission {
502 family: None,
503 ..base
504 }));
505 }
506
507 #[test]
508 fn required_policy_reports_unsupported_kernel() {
509 let decision = GpuDecision {
510 policy: GpuPolicy::Required,
511 kernel: GpuKernel::DenseXtWX,
512 use_gpu: false,
513 reason: "gpu-required-unsupported",
514 };
515 let err = decision.require_supported().unwrap_err();
516 assert!(err.contains("dense-xtwx"));
517 assert!(err.contains("gpu=required"));
518 }
519}