Coverage Report

Created: 2026-07-21 05:33

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::{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
426
fn nvml() -> Option<&'static Nvml> {
39
426
    if let Some(
nvml0
) = NVML.get() {
40
0
        return Some(nvml);
41
426
    }
42
426
    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
426
}
49
50
#[cfg(feature = "nvidia")]
51
215
pub(crate) fn detect() -> Vec<GpuInfo> {
52
215
    let Some(
nvml0
) = nvml() else {
53
215
        return Vec::new();
54
    };
55
0
    let Ok(count) = nvml.device_count() else {
56
0
        return Vec::new();
57
    };
58
0
    let mut gpus = Vec::new();
59
0
    for index in 0..count {
60
0
        let Ok(device) = nvml.device_by_index(index) else {
61
0
            continue;
62
        };
63
0
        let Ok(memory) = device.memory_info() else {
64
0
            continue;
65
        };
66
0
        gpus.push(GpuInfo {
67
0
            name: device.name().unwrap_or_else(|_| "NVIDIA GPU".to_string()),
68
0
            vendor: Vendor::Nvidia,
69
0
            total_bytes: memory.total,
70
0
            free_bytes: Some(memory.free),
71
0
            used_bytes: Some(memory.used),
72
        });
73
    }
74
0
    gpus
75
215
}
76
77
/// Host-wide CUDA properties, read from the same shared NVML handle as
78
/// [`detect`] so the process keeps exactly one NVML owner.
79
#[cfg(feature = "nvidia")]
80
211
pub(crate) fn cuda_host() -> Option<CudaHost> {
81
211
    let 
nvml0
= nvml()?;
82
83
    // Compute capability is per-device, but consumers use it to choose a build
84
    // target for the host, so device 0 is the meaningful answer.
85
0
    let capability = nvml
86
0
        .device_by_index(0)
87
0
        .ok()?
88
0
        .cuda_compute_capability()
89
0
        .ok()?;
90
    // NVML reports these as signed and a misbehaving driver can return
91
    // negatives. Reject them here rather than casting them into huge unsigned
92
    // values and making every caller re-derive the check.
93
0
    let compute_capability = ComputeCapability {
94
0
        major: u32::try_from(capability.major).ok()?,
95
0
        minor: u32::try_from(capability.minor).ok()?,
96
    };
97
98
0
    let packed = nvml.sys_cuda_driver_version().ok()?;
99
0
    if packed <= 0 {
100
0
        return None;
101
0
    }
102
    // NVML packs this as `major * 1000 + minor * 10`; let the binding unpack it
103
    // rather than open-coding the arithmetic.
104
0
    let driver_version = CudaVersion {
105
0
        major: u32::try_from(nvml_wrapper::cuda_driver_version_major(packed)).ok()?,
106
0
        minor: u32::try_from(nvml_wrapper::cuda_driver_version_minor(packed)).ok()?,
107
    };
108
109
0
    Some(CudaHost {
110
0
        compute_capability,
111
0
        driver_version,
112
0
    })
113
211
}
114
115
#[cfg(not(feature = "nvidia"))]
116
pub(crate) fn detect() -> Vec<crate::GpuInfo> {
117
    Vec::new()
118
}
119
120
#[cfg(not(feature = "nvidia"))]
121
pub(crate) fn cuda_host() -> Option<crate::CudaHost> {
122
    None
123
}