Skip to main content

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