gam_gpu/gpu_error.rs
1//! Typed error for the `src/gpu/*` modules.
2//!
3//! Every fallible path inside the GPU layer (driver dlopen, CUDA driver
4//! API calls, cuBLAS / cuSPARSE / cuSOLVER handle lifecycle, on-device
5//! allocations and memcpys, throughput calibration) constructs one of the
6//! variants below. Module-internal `Result<_, String>` surfaces convert
7//! via `From<GpuError> for String`, which preserves the exact bytes of
8//! the prior `format!` / `to_string` payloads so logged messages are
9//! byte-equivalent to the pre-refactor strings.
10//!
11//! Only the variants actually constructed by the GPU layer are kept.
12
13/// Typed error for `src/gpu/*.rs` operations.
14#[derive(Debug, Clone)]
15pub enum GpuError {
16 /// The CUDA driver shared library (`libcuda.so` / `nvcuda.dll` /
17 /// `libcuda.dylib`) or one of its sibling stubs (cuSOLVER, cuSPARSE)
18 /// could not be loaded from any of the searched candidates.
19 DriverLibraryUnavailable { reason: String },
20 /// A CUDA driver candidate exists or resolves through the platform loader,
21 /// but `dlopen`/`LoadLibrary` rejected it (invalid ABI, corrupt object,
22 /// missing transitive dependency, or loader-initializer failure). This is
23 /// a broken installation, never ordinary hardware absence.
24 DriverLibraryLoadFailed { reason: String },
25 /// The CUDA driver is present, but a mandatory runtime dependency such as
26 /// cuBLAS, cuSOLVER, or cuSPARSE is missing. This is an installation fault,
27 /// not the ordinary "this host has no CUDA device" absence state.
28 RuntimeDependencyUnavailable { reason: String },
29 /// A required CUDA / cuBLAS / cuSOLVER / cuSPARSE symbol was missing
30 /// from a loaded library (i.e. `libloading::Library::get` returned an
31 /// error for a name we need).
32 DriverSymbolMissing { reason: String },
33 /// A CUDA driver / cuSOLVER / cuSPARSE C API call returned a non-zero
34 /// status code, or a `cudarc` safe wrapper (context bind, stream
35 /// create, cuBLAS init, alloc, memcpy, gemm, synchronize) failed.
36 DriverCallFailed { reason: String },
37 /// Runtime throughput calibration produced an unusable measurement
38 /// (non-positive elapsed time, non-finite GB/s or GFLOPS).
39 CalibrationFailed { reason: String },
40 /// No device kernel exists for the requested GPU code path on this build.
41 /// Device kernels are added opportunistically as accelerations; the absence
42 /// of one is a permanently-possible, correctly-handled condition — not a
43 /// defect — because the CPU path it falls back to is the correct reference
44 /// computation. Callers treat this as a sentinel to fall back to the CPU
45 /// path silently (no panic, no error log). Distinct from `DriverCallFailed`
46 /// so the dispatcher can tell "no kernel for this path" apart from "the
47 /// device refused". Carries a short reason for diagnostics, e.g. the kernel
48 /// name. (Any GPU acceleration roadmap belongs in an issue, not here.)
49 NoDeviceKernel { reason: String },
50 /// `GpuPolicy::Required` was requested on a host with a genuine, typed CUDA
51 /// absence (unsupported platform, no driver, or no device). Probe faults
52 /// retain their original variant and never pass through this wrapper.
53 RequiredDeviceUnavailable { reason: String },
54}
55
56impl std::fmt::Display for GpuError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Self::DriverLibraryUnavailable { reason }
60 | Self::DriverLibraryLoadFailed { reason }
61 | Self::RuntimeDependencyUnavailable { reason }
62 | Self::DriverSymbolMissing { reason }
63 | Self::DriverCallFailed { reason }
64 | Self::CalibrationFailed { reason }
65 | Self::NoDeviceKernel { reason }
66 | Self::RequiredDeviceUnavailable { reason } => f.write_str(reason),
67 }
68 }
69}
70
71impl std::error::Error for GpuError {}
72
73impl From<GpuError> for String {
74 fn from(err: GpuError) -> String {
75 err.to_string()
76 }
77}
78
79/// Build a `GpuError::DriverCallFailed { reason: format!(...) }` value.
80///
81/// Collapses the ubiquitous
82/// `GpuError::DriverCallFailed { reason: format!("...: {err}") }`
83/// struct literal into a single call. The macro forwards every argument
84/// to `format!`, so callers retain full control over the message body,
85/// including positional / named captures and interpolation of the
86/// per-site `err` binding.
87#[macro_export]
88macro_rules! gpu_err {
89 ($($arg:tt)*) => {
90 $crate::gpu_error::GpuError::DriverCallFailed { reason: ::std::format!($($arg)*) }
91 };
92}
93
94/// `return Err(GpuError::DriverCallFailed { reason: format!(...) })`.
95///
96/// Collapses every early-return driver-call failure into a single
97/// statement. Use inside functions that return `Result<_, GpuError>`.
98#[macro_export]
99macro_rules! gpu_bail {
100 // The `gpu_err!` construction is inlined here rather than invoked as
101 // `$crate::gpu_err!`: because `lib.rs` includes this module tree via
102 // `include!`, the `#[macro_export]` `gpu_err` counts as macro-expanded,
103 // and referring to it by the absolute `$crate::` path trips a denied
104 // future-incompat lint. Keep this body in sync with `gpu_err!`.
105 ($($arg:tt)*) => {
106 return ::std::result::Result::Err(
107 $crate::gpu_error::GpuError::DriverCallFailed { reason: ::std::format!($($arg)*) },
108 )
109 };
110}
111
112/// Extension trait that attaches GPU-call context to any `Result<T, E>`
113/// whose error implements `Display`.
114///
115/// The two methods mirror the common shapes:
116/// * [`gpu_ctx`](GpuResultExt::gpu_ctx) appends `": {err}"` to a
117/// caller-supplied prefix. This is the vastly dominant shape across
118/// the GPU layer (~235 sites in the original audit).
119/// * [`gpu_ctx_with`](GpuResultExt::gpu_ctx_with) takes a closure that
120/// receives the underlying error by `&dyn Display` and returns the
121/// full reason string. Use it when the reason is not a simple
122/// `prefix: err` concatenation (e.g. multi-line, or with the error
123/// embedded mid-message).
124///
125/// **Cfg note**: The trait and its blanket impl are gated to
126/// `target_os = "linux"` so the symbol literally does not exist on
127/// non-Linux targets. Every callsite is inside a
128/// `#[cfg(target_os = "linux")]` block that wraps CUDA driver / cuBLAS /
129/// cuSOLVER calls; on non-Linux those blocks are erased and the trait
130/// would have no users. Cfg-gating the definition means a warning-fix
131/// sweep running on non-Linux cannot see "unused" callsites because the
132/// trait itself is absent — the consuming `use super::gpu_error::GpuResultExt;`
133/// imports must therefore be `#[cfg(target_os = "linux")]` to match, and
134/// that cfg-symmetry is the architectural contract that prevents the
135/// drop-the-import regression that broke the Linux build in #302.
136#[cfg(target_os = "linux")]
137pub trait GpuResultExt<T> {
138 /// Map the error to `GpuError::DriverCallFailed { reason: format!("{prefix}: {err}") }`.
139 fn gpu_ctx(self, prefix: &str) -> Result<T, GpuError>;
140
141 /// Map the error using a closure that takes the underlying error
142 /// (as `&dyn Display`) and returns the reason string.
143 fn gpu_ctx_with<F>(self, f: F) -> Result<T, GpuError>
144 where
145 F: FnOnce(&dyn std::fmt::Display) -> String;
146}
147
148#[cfg(target_os = "linux")]
149impl<T, E: std::fmt::Display> GpuResultExt<T> for Result<T, E> {
150 #[inline]
151 fn gpu_ctx(self, prefix: &str) -> Result<T, GpuError> {
152 self.map_err(|err| GpuError::DriverCallFailed {
153 reason: format!("{prefix}: {err}"),
154 })
155 }
156
157 #[inline]
158 fn gpu_ctx_with<F>(self, f: F) -> Result<T, GpuError>
159 where
160 F: FnOnce(&dyn std::fmt::Display) -> String,
161 {
162 self.map_err(|err| GpuError::DriverCallFailed { reason: f(&err) })
163 }
164}