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