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 | | } |