Coverage Report

Created: 2026-08-20 05:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
src/nvidia.rs
Line
Count
Source
1
// SPDX-License-Identifier: Apache-2.0
2
//! NVIDIA detection via NVML (`libnvidia-ml`), loaded at runtime from the
3
//! installed driver. The CUDA toolkit is **not** required and nothing links at
4
//! build time — `nvml-wrapper` `dlopen`s the library lazily, so a host without
5
//! an NVIDIA driver simply yields no GPUs.
6
//!
7
//! Gated behind the default `nvidia` feature; build with
8
//! `default-features = false` to drop the `nvml-wrapper` dependency entirely.
9
10
#[cfg(feature = "nvidia")]
11
use crate::{ArchTarget, ComputeCapability, CudaHost, CudaVersion, GpuInfo, Vendor};
12
#[cfg(feature = "nvidia")]
13
use nvml_wrapper::Nvml;
14
#[cfg(feature = "nvidia")]
15
use std::sync::OnceLock;
16
17
/// The process-wide NVML handle.
18
///
19
/// NVML is initialized at most once and **never shut down**. This is
20
/// deliberate, not an oversight: cycling `nvmlInit`/`nvmlShutdown` permanently
21
/// costs one file descriptor per cycle (an `eventfd` that shutdown does not
22
/// return), so a caller polling [`detect`](crate::detect) on a timer exhausts
23
/// its fd table and can no longer `accept()` connections. Holding one handle is
24
/// flat across calls, and queries against it still return live values — so
25
/// `memory_info()` readouts stay current.
26
///
27
/// Do not add a shutdown or make this handle droppable.
28
#[cfg(feature = "nvidia")]
29
static NVML: OnceLock<Nvml> = OnceLock::new();
30
31
/// The shared NVML handle, initializing it on first use.
32
///
33
/// Only *success* is cached. A failed init allocates no file descriptors, so
34
/// retrying costs nothing but a failed `dlopen`; caching the failure instead
35
/// would mean a host whose driver loads after the first call — or a daemon that
36
/// starts before the driver is up — reports "no NVIDIA GPU" until it restarts.
37
#[cfg(feature = "nvidia")]
38
430
fn nvml() -> Option<&'static Nvml> {
39
430
    if let Some(
nvml0
) = NVML.get() {
40
0
        return Some(nvml);
41
430
    }
42
430
    let 
nvml0
= Nvml::init().ok()?;
43
    // A concurrent first call may have won the race, in which case our handle
44
    // is dropped here and theirs is returned. `nvmlInit`/`nvmlShutdown` are
45
    // reference counted, so the winner's handle stays valid. This costs one fd,
46
    // once, and only when two threads make the very first call simultaneously.
47
0
    Some(NVML.get_or_init(|| nvml))
48
430
}
49
50
/// Read a device's compute capability, rejecting values NVML shouldn't produce.
51
///
52
/// NVML reports these as signed and a misbehaving driver can return negatives.
53
/// Reject them here rather than casting them into huge unsigned values and
54
/// making every caller re-derive the check.
55
#[cfg(feature = "nvidia")]
56
0
fn device_compute_capability(device: &nvml_wrapper::Device<'_>) -> Option<ComputeCapability> {
57
0
    let capability = device.cuda_compute_capability().ok()?;
58
    Some(ComputeCapability {
59
0
        major: u32::try_from(capability.major).ok()?,
60
0
        minor: u32::try_from(capability.minor).ok()?,
61
    })
62
0
}
63
64
#[cfg(feature = "nvidia")]
65
219
pub(crate) fn detect() -> Vec<GpuInfo> {
66
219
    let Some(
nvml0
) = nvml() else {
67
219
        return Vec::new();
68
    };
69
0
    let Ok(count) = nvml.device_count() else {
70
0
        return Vec::new();
71
    };
72
0
    let mut gpus = Vec::new();
73
0
    for index in 0..count {
74
0
        let Ok(device) = nvml.device_by_index(index) else {
75
0
            continue;
76
        };
77
0
        let Ok(memory) = device.memory_info() else {
78
0
            continue;
79
        };
80
0
        gpus.push(GpuInfo {
81
0
            name: device.name().unwrap_or_else(|_| "NVIDIA GPU".to_string()),
82
0
            vendor: Vendor::Nvidia,
83
0
            total_bytes: memory.total,
84
0
            free_bytes: Some(memory.free),
85
0
            used_bytes: Some(memory.used),
86
0
            arch_target: device_compute_capability(&device).map(ArchTarget::Sm),
87
        });
88
    }
89
0
    gpus
90
219
}
91
92
/// Host-wide CUDA properties, read from the same shared NVML handle as
93
/// [`detect`] so the process keeps exactly one NVML owner.
94
#[cfg(feature = "nvidia")]
95
211
pub(crate) fn cuda_host() -> Option<CudaHost> {
96
211
    let 
nvml0
= nvml()?;
97
98
    // Compute capability is per-device — and is also reported that way on
99
    // `GpuInfo` — but consumers use this struct to choose one build target for
100
    // the host, so device 0 is the meaningful answer here.
101
0
    let compute_capability = device_compute_capability(&nvml.device_by_index(0).ok()?)?;
102
103
0
    let packed = nvml.sys_cuda_driver_version().ok()?;
104
0
    if packed <= 0 {
105
0
        return None;
106
0
    }
107
    // NVML packs this as `major * 1000 + minor * 10`; let the binding unpack it
108
    // rather than open-coding the arithmetic.
109
0
    let driver_version = CudaVersion {
110
0
        major: u32::try_from(nvml_wrapper::cuda_driver_version_major(packed)).ok()?,
111
0
        minor: u32::try_from(nvml_wrapper::cuda_driver_version_minor(packed)).ok()?,
112
    };
113
114
0
    Some(CudaHost {
115
0
        compute_capability,
116
0
        driver_version,
117
0
    })
118
211
}
119
120
#[cfg(not(feature = "nvidia"))]
121
pub(crate) fn detect() -> Vec<crate::GpuInfo> {
122
    Vec::new()
123
}
124
125
#[cfg(not(feature = "nvidia"))]
126
pub(crate) fn cuda_host() -> Option<crate::CudaHost> {
127
    None
128
}