1#[cfg(target_os = "linux")]
2use std::cell::Cell;
3#[cfg(target_os = "linux")]
4use std::collections::HashMap;
5#[cfg(target_os = "linux")]
6use std::panic::{self, AssertUnwindSafe, catch_unwind};
7use std::sync::OnceLock;
8use std::sync::atomic::{AtomicU64, Ordering};
9#[cfg(target_os = "linux")]
10use std::sync::{Arc, Mutex};
11
12use super::device::GpuDeviceInfo;
13use super::gpu_error::GpuError;
14use super::policy::GpuDispatchPolicy;
15#[cfg(target_os = "linux")]
16use cudarc::driver::{CudaContext, result, sys};
17
18#[path = "runtime_diagnostics.rs"]
19pub(crate) mod diagnostics;
20
21#[derive(Clone, Debug)]
22#[must_use]
23pub struct GpuRuntime {
24 pub device: GpuDeviceInfo,
27 pub devices: Vec<GpuDeviceInfo>,
29 pub policy: GpuDispatchPolicy,
30 pub memory_budget_bytes: usize,
31}
32
33static CPU_REASON: OnceLock<String> = OnceLock::new();
34
35#[derive(Clone, Debug, Eq, PartialEq)]
40pub enum GpuAbsence {
41 UnsupportedPlatform,
42 DriverUnavailable { reason: String },
43 NoDevice { reason: String },
44}
45
46impl std::fmt::Display for GpuAbsence {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::UnsupportedPlatform => {
50 f.write_str("CUDA support is unavailable on this platform")
51 }
52 Self::DriverUnavailable { reason } | Self::NoDevice { reason } => f.write_str(reason),
53 }
54 }
55}
56
57#[derive(Debug)]
59pub enum GpuAvailability {
60 Available(GpuRuntime),
61 Absent(GpuAbsence),
62}
63
64#[derive(Clone, Copy, Debug)]
66pub enum GpuAvailabilityRef<'a> {
67 Available(&'a GpuRuntime),
68 Absent(&'a GpuAbsence),
69}
70
71static RESOLUTION_CALLS: AtomicU64 = AtomicU64::new(0);
84
85#[cfg(target_os = "linux")]
86thread_local! {
87 static CUDARC_RECOVERY_ACTIVE: Cell<bool> = const { Cell::new(false) };
88}
89
90#[cfg(target_os = "linux")]
91fn panic_message(payload: &(dyn std::any::Any + Send)) -> Option<&str> {
92 payload
93 .downcast_ref::<&'static str>()
94 .copied()
95 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
96}
97
98#[cfg(target_os = "linux")]
101fn install_cudarc_panic_filter() {
102 static HOOK_INSTALLED: OnceLock<()> = OnceLock::new();
103 HOOK_INSTALLED.get_or_init(|| {
104 let prior = panic::take_hook();
105 panic::set_hook(Box::new(move |info| {
106 if cfg!(panic = "unwind")
107 && CUDARC_RECOVERY_ACTIVE.with(Cell::get)
108 && panic_message(info.payload())
109 .is_some_and(|message| message.starts_with("Unable to dynamically load"))
110 {
111 return;
112 }
113 prior(info);
114 }));
115 });
116}
117
118#[cfg(target_os = "linux")]
121fn catch_cudarc<T>(call: impl FnOnce() -> T) -> Result<T, String> {
122 install_cudarc_panic_filter();
123 struct RecoveryScope(bool);
124 impl Drop for RecoveryScope {
125 fn drop(&mut self) {
126 CUDARC_RECOVERY_ACTIVE.with(|active| active.set(self.0));
127 }
128 }
129 let scope = RecoveryScope(CUDARC_RECOVERY_ACTIVE.with(|active| active.replace(true)));
130 let outcome = catch_unwind(AssertUnwindSafe(call));
131 drop(scope);
132 match outcome {
133 Ok(value) => Ok(value),
134 Err(payload) => match panic_message(payload.as_ref()) {
135 Some(message) if message.starts_with("Unable to dynamically load") => {
136 Err(message.to_owned())
137 }
138 _ => panic::resume_unwind(payload),
139 },
140 }
141}
142
143impl GpuRuntime {
144 pub fn probe() -> Result<GpuAvailability, GpuError> {
145 #[cfg(target_os = "linux")]
146 {
147 catch_cudarc(Self::probe_devices)
148 .map_err(|reason| GpuError::RuntimeDependencyUnavailable { reason })?
149 }
150 #[cfg(not(target_os = "linux"))]
151 Self::probe_devices()
152 }
153
154 fn probe_devices() -> Result<GpuAvailability, GpuError> {
155 #[cfg(not(target_os = "linux"))]
156 {
157 let reason = "CUDA support not compiled into this build";
158 Self::record_cpu_reason(reason);
159 diagnostics::log_cuda_disabled(reason);
160 return Ok(GpuAvailability::Absent(GpuAbsence::UnsupportedPlatform));
161 }
162
163 #[cfg(target_os = "linux")]
164 {
165 let primary_ready = cuda_context_for(0).is_some();
192 log::trace!("[GPU] probe pre-init primary context + runtime: {primary_ready}");
193 match crate::driver::preload_cuda_driver() {
194 Ok(()) => {}
195 Err(GpuError::DriverLibraryUnavailable { reason }) => {
196 Self::record_cpu_reason(reason.clone());
197 log::info!("[GPU] CUDA acceleration disabled: {reason}");
198 diagnostics::log_cuda_disabled(&reason);
199 return Ok(GpuAvailability::Absent(GpuAbsence::DriverUnavailable {
200 reason,
201 }));
202 }
203 Err(error) => return Err(error),
204 }
205
206 for stem in ["cublas", "cusolver", "cusparse"] {
219 if let Err(error) = crate::driver::require_cuda_compute_library(stem) {
220 let reason = format!("lib{stem} unavailable: {error}");
221 Self::record_cpu_reason(reason.clone());
222 log::info!("[GPU] CUDA acceleration disabled: {reason}");
223 diagnostics::log_cuda_disabled(&reason);
224 return Err(GpuError::RuntimeDependencyUnavailable { reason });
225 }
226 }
227
228 let device_count = match catch_cudarc(CudaContext::device_count) {
236 Err(_) => {
237 return Err(GpuError::DriverCallFailed {
238 reason: "cudarc failed after the CUDA driver preflight succeeded"
239 .to_string(),
240 });
241 }
242 Ok(Ok(count)) => count,
243 Ok(Err(error)) => {
244 if let Some(absence) = absence_from_driver_init_error(&error) {
253 let reason = absence.to_string();
254 Self::record_cpu_reason(reason.clone());
255 log::info!("[GPU] CUDA acceleration disabled: {reason}");
256 diagnostics::log_cuda_disabled(&reason);
257 return Ok(GpuAvailability::Absent(absence));
258 }
259 return Err(GpuError::DriverCallFailed {
260 reason: error.to_string(),
261 });
262 }
263 };
264 if device_count <= 0 {
265 let reason = "CUDA driver reported no devices";
266 Self::record_cpu_reason(reason);
267 diagnostics::log_cuda_disabled(reason);
268 return Ok(GpuAvailability::Absent(GpuAbsence::NoDevice {
269 reason: reason.to_string(),
270 }));
271 }
272
273 let mut devices = Vec::new();
274 for ordinal in
275 0..usize::try_from(device_count).map_err(|_| GpuError::DriverCallFailed {
276 reason: "negative CUDA device count".into(),
277 })?
278 {
279 let ctx = cuda_context_for(ordinal).ok_or_else(|| {
280 gpu_err!("failed to create CUDA context for device {ordinal}")
281 })?;
282 catch_cudarc(|| ctx.bind_to_thread())
283 .map_err(|_| GpuError::DriverCallFailed {
284 reason: "CUDA context binding panicked after driver discovery".to_string(),
285 })?
286 .map_err(|err| GpuError::DriverCallFailed {
287 reason: err.to_string(),
288 })?;
289 devices.push(catch_cudarc(|| cuda_device_info(ordinal, &ctx)).map_err(
290 |_| GpuError::DriverCallFailed {
291 reason:
292 "CUDA device inspection panicked after driver discovery".to_string(),
293 },
294 )??);
295 }
296
297 devices.sort_by(|a, b| b.score().total_cmp(&a.score()));
298 let Some(device) = devices.first().cloned() else {
299 Self::record_cpu_reason("CUDA driver reported no usable devices");
300 diagnostics::log_cuda_disabled("CUDA driver reported no usable devices");
301 return Ok(GpuAvailability::Absent(GpuAbsence::NoDevice {
302 reason: "CUDA driver reported no usable devices".to_string(),
303 }));
304 };
305
306 let policy = crate::calibration::calibrated_policy_for_device(&device);
307 let memory_budget_bytes = device.memory_budget_bytes();
308 diagnostics::log_cuda_enabled(&device, &policy);
309 diagnostics::log_cuda_pool(&devices);
310
311 Ok(GpuAvailability::Available(Self {
312 device,
313 devices,
314 policy,
315 memory_budget_bytes,
316 }))
317 }
318 }
319
320 pub fn availability() -> Result<GpuAvailabilityRef<'static>, GpuError> {
322 RESOLUTION_CALLS.fetch_add(1, Ordering::Relaxed);
326 static RUNTIME: OnceLock<Result<GpuAvailability, GpuError>> = OnceLock::new();
327 let cached = RUNTIME.get_or_init(|| {
328 let outcome = Self::probe();
329 if let Err(error) = &outcome {
330 let reason = error.to_string();
331 Self::record_cpu_reason(reason.clone());
332 diagnostics::log_cuda_disabled(&reason);
333 }
334 if matches!(&outcome, Ok(GpuAvailability::Available(_))) {
347 gam_linalg::gpu_hook::register_gpu_dispatch(Box::new(
348 super::linalg_dispatch::CudaGemmDispatch,
349 ));
350 }
351 outcome
352 });
353 match cached {
354 Ok(GpuAvailability::Available(runtime)) => Ok(GpuAvailabilityRef::Available(runtime)),
355 Ok(GpuAvailability::Absent(reason)) => Ok(GpuAvailabilityRef::Absent(reason)),
356 Err(error) => Err(error.clone()),
357 }
358 }
359
360 pub fn resolve(policy: super::GpuPolicy) -> Result<Option<&'static Self>, GpuError> {
364 if policy == super::GpuPolicy::Off {
365 return Ok(None);
366 }
367 Self::resolve_availability(policy, Self::availability())
368 }
369
370 fn resolve_availability<'a>(
371 policy: super::GpuPolicy,
372 availability: Result<GpuAvailabilityRef<'a>, GpuError>,
373 ) -> Result<Option<&'a Self>, GpuError> {
374 match availability? {
375 GpuAvailabilityRef::Available(runtime) => Ok(Some(runtime)),
376 GpuAvailabilityRef::Absent(_reason) if policy == super::GpuPolicy::Auto => Ok(None),
377 GpuAvailabilityRef::Absent(reason) => Err(GpuError::RequiredDeviceUnavailable {
378 reason: reason.to_string(),
379 }),
380 }
381 }
382
383 pub fn require() -> Result<&'static Self, GpuError> {
385 Self::resolve(super::GpuPolicy::Required)?.ok_or_else(|| {
386 GpuError::RequiredDeviceUnavailable {
387 reason: "required CUDA runtime resolved to an absent state".to_string(),
388 }
389 })
390 }
391
392 pub fn resolve_if_fused_batch_exceeds_floor(
402 policy: super::GpuPolicy,
403 rows: usize,
404 ) -> Result<Option<&'static Self>, GpuError> {
405 if rows < GpuDispatchPolicy::MIN_CALIBRATABLE_FUSED_KERNEL_N {
406 return Ok(None);
407 }
408 Self::resolve(policy)
409 }
410
411 #[must_use]
412 pub fn policy(&self) -> &GpuDispatchPolicy {
413 &self.policy
414 }
415
416 #[must_use]
417 pub fn selected_device(&self) -> &GpuDeviceInfo {
418 &self.device
419 }
420
421 #[must_use]
422 pub(crate) fn cpu_reason() -> Option<&'static str> {
423 CPU_REASON.get().map(String::as_str)
424 }
425
426 fn record_cpu_reason(reason: impl Into<String>) {
427 if let Err(dropped) = CPU_REASON.set(reason.into()) {
430 log::debug!(
431 "CPU fallback reason already recorded as {:?}; keeping it and dropping '{dropped}'",
432 CPU_REASON.get().map(String::as_str)
433 );
434 }
435 }
436}
437
438#[cfg(target_os = "linux")]
453fn absence_from_driver_init_error(error: &result::DriverError) -> Option<GpuAbsence> {
454 use sys::cudaError_enum as CudaErrorCode;
455 let code = error.0;
461 let classification = match code {
462 CudaErrorCode::CUDA_ERROR_NO_DEVICE => {
463 return Some(GpuAbsence::NoDevice {
464 reason: format!(
465 "CUDA driver initialized but reports no attached device ({code:?})"
466 ),
467 });
468 }
469 CudaErrorCode::CUDA_ERROR_STUB_LIBRARY => {
470 "the loaded libcuda is a linker stub, not a real driver"
471 }
472 CudaErrorCode::CUDA_ERROR_SYSTEM_NOT_READY => {
477 "the CUDA system is not ready (kernel driver or fabric daemon not running)"
478 }
479 CudaErrorCode::CUDA_ERROR_SYSTEM_DRIVER_MISMATCH => {
480 "the CUDA userland libraries do not match the host kernel driver"
481 }
482 CudaErrorCode::CUDA_ERROR_COMPAT_NOT_SUPPORTED_ON_DEVICE => {
483 "CUDA forward-compatibility mode is not supported on the visible device"
484 }
485 _ => return None,
486 };
487 Some(GpuAbsence::DriverUnavailable {
488 reason: format!("CUDA initialization refused: {classification} ({code:?})"),
489 })
490}
491
492#[cfg(target_os = "linux")]
505fn ensure_cuda_runtime_device(ordinal: usize) {
506 let Ok(o) = i32::try_from(ordinal) else {
507 return;
508 };
509 let set_rc = unsafe { cudarc::runtime::sys::cudaSetDevice(o) };
512 log::trace!("[GPU] runtime cudaSetDevice({o}) -> {set_rc:?}");
513 let mut p: *mut core::ffi::c_void = core::ptr::null_mut();
522 let malloc_rc = unsafe { cudarc::runtime::sys::cudaMalloc(&mut p as *mut _ as *mut _, 256) };
524 log::trace!("[GPU] runtime cudaMalloc -> {malloc_rc:?}");
525 if !p.is_null() {
526 let free_rc = unsafe { cudarc::runtime::sys::cudaFree(p) };
528 log::trace!("[GPU] runtime cudaFree -> {free_rc:?}");
529 }
530}
531
532#[cfg(target_os = "linux")]
533thread_local! {
534 static BOUND_RUNTIME_ORDINAL: Cell<Option<usize>> = const { Cell::new(None) };
549}
550
551#[cfg(target_os = "linux")]
567fn bind_and_touch_runtime(ordinal: usize, ctx: &Arc<CudaContext>) {
568 if BOUND_RUNTIME_ORDINAL.with(Cell::get) == Some(ordinal) {
569 return;
570 }
571 let bound = catch_cudarc(|| ctx.bind_to_thread());
572 log::trace!(
573 "[GPU] cuda_context_for bind ok={} ordinal={ordinal}",
574 matches!(bound, Ok(Ok(())))
575 );
576 ensure_cuda_runtime_device(ordinal);
577 if matches!(bound, Ok(Ok(()))) {
581 BOUND_RUNTIME_ORDINAL.with(|c| c.set(Some(ordinal)));
582 }
583}
584
585#[cfg(target_os = "linux")]
586pub fn cuda_context_for(ordinal: usize) -> Option<Arc<CudaContext>> {
587 static CONTEXTS: OnceLock<Mutex<HashMap<usize, Arc<CudaContext>>>> = OnceLock::new();
588 let contexts = CONTEXTS.get_or_init(|| Mutex::new(HashMap::new()));
589 if let Some(ctx) = contexts.lock().ok()?.get(&ordinal).cloned() {
590 bind_and_touch_runtime(ordinal, &ctx);
591 return Some(ctx);
592 }
593 let ctx = catch_cudarc(|| CudaContext::new(ordinal)).ok()?.ok()?;
597 let out = {
598 let mut guard = contexts.lock().ok()?;
599 guard.entry(ordinal).or_insert_with(|| ctx.clone()).clone()
600 };
601 bind_and_touch_runtime(ordinal, &out);
606 Some(out)
607}
608
609#[cfg(target_os = "linux")]
610fn cuda_device_info(ordinal: usize, ctx: &CudaContext) -> Result<GpuDeviceInfo, GpuError> {
611 result::init().map_err(|err| GpuError::DriverCallFailed {
612 reason: err.to_string(),
613 })?;
614 let device =
615 result::device::get(
616 i32::try_from(ordinal).map_err(|_| GpuError::DriverCallFailed {
617 reason: "device ordinal overflow".into(),
618 })?,
619 )
620 .map_err(|err| GpuError::DriverCallFailed {
621 reason: err.to_string(),
622 })?;
623 let attr = |attribute| -> Result<i32, GpuError> {
624 unsafe { result::device::get_attribute(device, attribute) }.map_err(|err| {
626 GpuError::DriverCallFailed {
627 reason: err.to_string(),
628 }
629 })
630 };
631 let (free_mem_bytes, total_mem_bytes) =
632 ctx.mem_get_info()
633 .map_err(|err| GpuError::DriverCallFailed {
634 reason: err.to_string(),
635 })?;
636 let major = attr(sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR)?;
637 let minor = attr(sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR)?;
638 Ok(GpuDeviceInfo {
639 ordinal,
640 name: result::device::get_name(device).unwrap_or_else(|err| {
641 log::debug!(
642 "CUDA device {ordinal}: name query failed ({err}); using a positional label"
643 );
644 format!("CUDA device {ordinal}")
645 }),
646 capability: super::device::GpuCapability::from_compute_capability(major, minor),
647 sm_count: attr(sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT)?,
648 max_threads_per_sm: attr(
649 sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_MAX_THREADS_PER_MULTIPROCESSOR,
650 )?,
651 max_shared_mem_per_block: attr(
652 sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_MAX_SHARED_MEMORY_PER_BLOCK,
653 )
654 .unwrap_or(0) as usize,
655 l2_cache_bytes: attr(sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_L2_CACHE_SIZE)
656 .unwrap_or(0) as usize,
657 total_mem_bytes,
658 free_mem_bytes,
659 ecc_enabled: attr(sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_ECC_ENABLED)
660 .unwrap_or(0)
661 != 0,
662 integrated: attr(sys::CUdevice_attribute_enum::CU_DEVICE_ATTRIBUTE_INTEGRATED).unwrap_or(0)
663 != 0,
664 mig_mode: false,
665 })
666}
667
668#[cfg(test)]
669mod policy_resolution_contract_tests {
670 use super::*;
671 use crate::GpuPolicy;
672
673 #[cfg(target_os = "linux")]
676 #[test]
677 fn cudarc_loader_panic_diagnostics_follow_recovery_scope() {
678 const CHILD_MODE_PREFIX: &str = "__gam_cudarc_child_";
679 const LOADER_PANIC: &str = "Unable to dynamically load synthetic CUDA library";
680 if let Some(mode) = std::env::args()
681 .find_map(|argument| argument.strip_prefix(CHILD_MODE_PREFIX).map(str::to_owned))
682 {
683 install_cudarc_panic_filter();
684 match mode.as_str() {
685 "caught" => {
686 assert_eq!(
687 catch_cudarc::<()>(|| panic!("{LOADER_PANIC}")),
688 Err(LOADER_PANIC.into()),
689 );
690 assert!(!CUDARC_RECOVERY_ACTIVE.with(Cell::get));
691 }
692 "nested" => {
693 let outer = catch_cudarc::<()>(|| {
694 assert!(catch_cudarc::<()>(|| panic!("{LOADER_PANIC}")).is_err());
695 assert!(CUDARC_RECOVERY_ACTIVE.with(Cell::get));
696 panic!("{LOADER_PANIC}");
697 });
698 assert_eq!(outer, Err(LOADER_PANIC.into()));
699 assert!(!CUDARC_RECOVERY_ACTIVE.with(Cell::get));
700 }
701 "after" => {
702 assert!(catch_cudarc::<()>(|| panic!("{LOADER_PANIC}")).is_err());
703 panic!("{LOADER_PANIC}");
704 }
705 "other_thread" => {
706 catch_cudarc(|| {
707 assert!(
708 std::thread::spawn(|| panic!("{LOADER_PANIC}"))
709 .join()
710 .is_err()
711 );
712 })
713 .expect("a different thread's panic must not enter this recovery");
714 }
715 "unrelated" => {
716 catch_cudarc::<()>(|| panic!("unrelated failure"))
717 .expect("unrelated panics must unwind");
718 }
719 "unguarded" => panic!("{LOADER_PANIC}"),
720 _ => panic!("unknown subprocess mode: {mode}"),
721 }
722 return;
723 }
724 for (mode, succeeds, diagnostic) in [
725 ("caught", true, None),
726 ("nested", true, None),
727 ("after", false, Some(LOADER_PANIC)),
728 ("other_thread", true, Some(LOADER_PANIC)),
729 ("unrelated", false, Some("unrelated failure")),
730 ("unguarded", false, Some(LOADER_PANIC)),
731 ] {
732 let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
733 .args([
734 "--exact",
735 "device_runtime::policy_resolution_contract_tests::cudarc_loader_panic_diagnostics_follow_recovery_scope",
736 "--nocapture",
737 ])
738 .args(["--skip", &format!("{CHILD_MODE_PREFIX}{mode}")])
741 .output()
742 .expect("run hook regression subprocess");
743 let stderr = String::from_utf8_lossy(&output.stderr);
744 assert_eq!(output.status.success(), succeeds, "mode={mode}: {stderr}");
745 assert!(String::from_utf8_lossy(&output.stdout).contains("running 1 test"));
746 match diagnostic {
747 Some(message) => assert!(stderr.contains(message), "mode={mode}: {stderr}"),
748 None => assert!(stderr.is_empty(), "mode={mode}: {stderr}"),
749 }
750 }
751 }
752
753 #[test]
754 fn auto_maps_only_typed_absence_to_none() {
755 let absence = GpuAbsence::NoDevice {
756 reason: "synthetic device-free absence".to_string(),
757 };
758 let resolved = GpuRuntime::resolve_availability(
759 GpuPolicy::Auto,
760 Ok(GpuAvailabilityRef::Absent(&absence)),
761 )
762 .expect("typed absence is expected under Auto");
763 assert!(resolved.is_none());
764 }
765
766 #[test]
767 fn required_turns_only_typed_absence_into_required_unavailable() {
768 let absence = GpuAbsence::DriverUnavailable {
769 reason: "synthetic missing driver".to_string(),
770 };
771 let error = GpuRuntime::resolve_availability(
772 GpuPolicy::Required,
773 Ok(GpuAvailabilityRef::Absent(&absence)),
774 )
775 .expect_err("Required must reject typed absence");
776 assert!(matches!(
777 error,
778 GpuError::RequiredDeviceUnavailable { ref reason }
779 if reason == "synthetic missing driver"
780 ));
781 }
782
783 #[cfg(target_os = "linux")]
788 #[test]
789 fn driver_mismatch_at_init_is_typed_absence_not_a_fault() {
790 for code in [
791 sys::cudaError_enum::CUDA_ERROR_SYSTEM_DRIVER_MISMATCH,
792 sys::cudaError_enum::CUDA_ERROR_STUB_LIBRARY,
793 sys::cudaError_enum::CUDA_ERROR_SYSTEM_NOT_READY,
794 sys::cudaError_enum::CUDA_ERROR_COMPAT_NOT_SUPPORTED_ON_DEVICE,
795 ] {
796 let absence = absence_from_driver_init_error(&result::DriverError(code))
797 .unwrap_or_else(|| panic!("{code:?} is an environment fact, not a device fault"));
798 assert!(
799 matches!(absence, GpuAbsence::DriverUnavailable { .. }),
800 "{code:?} must classify as an unavailable driver"
801 );
802 let resolved = GpuRuntime::resolve_availability(
803 GpuPolicy::Auto,
804 Ok(GpuAvailabilityRef::Absent(&absence)),
805 )
806 .expect("Auto must accept driver-environment absence");
807 assert!(resolved.is_none(), "Auto must fall back to CPU on {code:?}");
808 let required_error = GpuRuntime::resolve_availability(
809 GpuPolicy::Required,
810 Ok(GpuAvailabilityRef::Absent(&absence)),
811 )
812 .expect_err("Required must refuse driver-environment absence");
813 assert!(
814 matches!(required_error, GpuError::RequiredDeviceUnavailable { .. }),
815 "Required must carry the environment diagnosis for {code:?}"
816 );
817 }
818 let no_device = absence_from_driver_init_error(&result::DriverError(
819 sys::cudaError_enum::CUDA_ERROR_NO_DEVICE,
820 ))
821 .expect("no attached device is an environment fact");
822 assert!(matches!(no_device, GpuAbsence::NoDevice { .. }));
823 }
824
825 #[cfg(target_os = "linux")]
829 #[test]
830 fn present_device_faults_never_classify_as_absence() {
831 for code in [
832 sys::cudaError_enum::CUDA_ERROR_ILLEGAL_ADDRESS,
833 sys::cudaError_enum::CUDA_ERROR_OUT_OF_MEMORY,
834 sys::cudaError_enum::CUDA_ERROR_NOT_INITIALIZED,
835 sys::cudaError_enum::CUDA_ERROR_ECC_UNCORRECTABLE,
836 sys::cudaError_enum::CUDA_ERROR_UNKNOWN,
837 ] {
838 assert!(
839 absence_from_driver_init_error(&result::DriverError(code)).is_none(),
840 "{code:?} is a fault of present hardware and must stay a probe fault"
841 );
842 }
843 }
844
845 #[test]
846 fn auto_and_required_preserve_probe_fault_variants() {
847 for policy in [GpuPolicy::Auto, GpuPolicy::Required] {
848 let error = GpuRuntime::resolve_availability(
849 policy,
850 Err(GpuError::RuntimeDependencyUnavailable {
851 reason: "synthetic missing cuBLAS".to_string(),
852 }),
853 )
854 .expect_err("probe faults must never project to absence");
855 assert!(matches!(
856 error,
857 GpuError::RuntimeDependencyUnavailable { ref reason }
858 if reason == "synthetic missing cuBLAS"
859 ));
860 }
861 }
862}