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