use std::collections::HashMap;
#[cfg(target_os = "linux")]
use std::ffi::OsStr;
use nvml_wrapper::bitmasks::device::ThrottleReasons as NvmlThrottle;
#[cfg(target_os = "windows")]
use nvml_wrapper::enum_wrappers::device::DriverModel;
use nvml_wrapper::enum_wrappers::device::{Clock, TemperatureSensor, TemperatureThreshold};
use nvml_wrapper::enums::device::UsedGpuMemory;
#[cfg(any(target_os = "windows", test))]
use nvml_wrapper::error::NvmlError;
use nvml_wrapper::Nvml;
use crate::backend::{BackendError, GpuBackend};
use crate::model::{
now_ms, DeviceId, DynamicSample, ProcessKind, ProcessSample, StaticInfo, ThrottleReasons,
Vendor,
};
fn opt<T, E>(r: Result<T, E>) -> Option<T> {
r.ok()
}
#[cfg(any(target_os = "linux", test))]
fn is_wsl(osrelease: &str) -> bool {
osrelease.to_ascii_lowercase().contains("microsoft")
}
#[cfg(target_os = "linux")]
fn wsl_process_hint() -> Option<String> {
#[cfg(target_os = "linux")]
if std::fs::read_to_string("/proc/sys/kernel/osrelease").is_ok_and(|rel| is_wsl(&rel)) {
return Some(
"per-process GPU info is unavailable under WSL2 (driver-level limitation) — \
device metrics are unaffected"
.into(),
);
}
None
}
#[cfg(test)]
fn explicit_lib_path_for(target_os: &str) -> Option<&'static str> {
match target_os {
"linux" => Some("libnvidia-ml.so.1"),
_ => None,
}
}
#[cfg(any(target_os = "windows", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DriverModelClass {
Wddm,
Tcc,
UnknownVariant,
}
#[cfg(any(target_os = "windows", test))]
fn classify_driver_model_err(e: &NvmlError) -> DriverModelClass {
match e {
NvmlError::UnexpectedVariant(_) => DriverModelClass::UnknownVariant,
_ => DriverModelClass::Wddm,
}
}
#[cfg(any(target_os = "windows", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(all(target_os = "windows", not(feature = "wddm")), allow(dead_code))]
enum PdhAttribution {
Available,
NoLuidMatch,
NoPdh,
}
#[cfg(any(target_os = "windows", test))]
fn windows_process_hint(model: DriverModelClass, pdh: PdhAttribution) -> Option<String> {
match model {
DriverModelClass::Wddm => Some(match pdh {
PdhAttribution::Available => {
"per-process VRAM/utilization come from Windows (WDDM) GPU performance \
counters, not the NVIDIA driver — NVML cannot see them under WDDM"
.into()
}
PdhAttribution::NoLuidMatch => {
"could not attribute per-process GPU data (LUID\u{2194}PCI match failed) \
— per-process VRAM/utilization unavailable; device metrics unaffected"
.into()
}
PdhAttribution::NoPdh => "per-process GPU stats unavailable: no WDDM 2.0 GPU/driver \
(Windows exposes them via GPU performance counters)"
.into(),
}),
DriverModelClass::Tcc => None,
DriverModelClass::UnknownVariant => Some(
"driver reports an unknown compute driver model (newer than this build) — \
per-process GPU data is shown as NVML reports it and may be incomplete"
.into(),
),
}
}
#[cfg(any(target_os = "windows", test))]
fn windows_source_caveat(pdh: PdhAttribution) -> Option<String> {
Some(match pdh {
PdhAttribution::Available => {
"memory used is Windows' VidMm dedicated usage (PDH), falling back to the \
NVIDIA driver's view of WDDM-virtualized memory when PDH has no sample"
.into()
}
PdhAttribution::NoLuidMatch | PdhAttribution::NoPdh => {
"memory used is the NVIDIA driver's view of WDDM-virtualized memory and can \
diverge from the OS (VidMm) number"
.into()
}
})
}
#[cfg(target_os = "windows")]
fn device_driver_model(d: &nvml_wrapper::Device<'_>) -> DriverModelClass {
match d.driver_model() {
Ok(state) => match state.current {
DriverModel::WDDM => DriverModelClass::Wddm,
DriverModel::WDM => DriverModelClass::Tcc,
},
Err(e) => classify_driver_model_err(&e),
}
}
#[cfg(any(target_os = "windows", test))]
fn should_retry_v2(e: &NvmlError) -> bool {
matches!(e, NvmlError::FailedToLoadSymbol(_))
}
fn used_gpu_memory_bytes(m: &UsedGpuMemory) -> Option<u64> {
match m {
UsedGpuMemory::Used(b) => Some(*b),
UsedGpuMemory::Unavailable => None,
}
}
pub struct NvidiaBackend {
nvml: Nvml,
devs: Vec<(u32, DeviceId)>,
#[cfg(all(target_os = "windows", feature = "wddm"))]
luids: Vec<Option<(i32, u32)>>,
#[cfg(target_os = "linux")]
last_util_ts: HashMap<u32, u64>,
#[cfg(target_os = "linux")]
process_hint: Option<String>,
#[cfg(target_os = "linux")]
cpu: crate::proc_meta::CpuTracker,
}
impl NvidiaBackend {
pub fn init() -> Result<Self, BackendError> {
#[cfg(target_os = "linux")]
let nvml = Nvml::builder()
.lib_path(OsStr::new("libnvidia-ml.so.1"))
.init()
.or_else(|_| Nvml::init())
.map_err(|e| BackendError::Unavailable(format!("NVML unavailable: {e}")))?;
#[cfg(target_os = "windows")]
let nvml = Nvml::init()
.map_err(|e| BackendError::Unavailable(format!("NVML unavailable: {e}")))?;
let count = nvml
.device_count()
.map_err(|e| BackendError::Unavailable(format!("NVML device count: {e}")))?;
let mut devs = Vec::new();
for i in 0..count {
let Ok(dev) = nvml.device_by_index(i) else {
continue;
};
let id = match dev.pci_info() {
Ok(pci) => DeviceId(pci.bus_id.to_lowercase()),
Err(_) => DeviceId(format!("nvml:{i}")),
};
devs.push((i, id));
}
if devs.is_empty() {
return Err(BackendError::Unavailable(
"NVML loaded but no devices".into(),
));
}
#[cfg(all(target_os = "windows", feature = "wddm"))]
let luids: Vec<Option<(i32, u32)>> = {
let _ = crate::wddm::pdh::shared().snapshot(now_ms());
let adapters = crate::wddm::adapters::enumerate();
devs.iter()
.map(|(_, id)| {
let key = crate::model::normalize_pci_id(&id.0)?;
adapters
.iter()
.find(|a| {
a.pci_bdf
.as_deref()
.and_then(crate::model::normalize_pci_id)
.as_deref()
== Some(key.as_str())
})
.map(|a| (a.luid_high, a.luid_low))
})
.collect()
};
Ok(Self {
nvml,
devs,
#[cfg(all(target_os = "windows", feature = "wddm"))]
luids,
#[cfg(target_os = "linux")]
last_util_ts: HashMap::new(),
#[cfg(target_os = "linux")]
process_hint: wsl_process_hint(),
#[cfg(target_os = "linux")]
cpu: crate::proc_meta::CpuTracker::new(),
})
}
fn index_of(&self, dev: &DeviceId) -> Result<u32, BackendError> {
self.devs
.iter()
.find(|(_, id)| id == dev)
.map(|(i, _)| *i)
.ok_or_else(|| BackendError::DeviceNotFound(dev.clone()))
}
#[cfg(all(target_os = "windows", feature = "wddm"))]
fn luid_of(&self, dev: &DeviceId) -> Option<(i32, u32)> {
self.devs
.iter()
.position(|(_, id)| id == dev)
.and_then(|p| self.luids.get(p).copied().flatten())
}
fn process_name(&self, pid: u32) -> String {
if let Ok(name) = self.nvml.sys_process_name(pid, 128) {
if let Some(base) = name.rsplit(['/', '\\']).next() {
if !base.is_empty() {
return base.to_string();
}
}
return name;
}
#[cfg(target_os = "linux")]
if let Ok(comm) = std::fs::read_to_string(format!("/proc/{pid}/comm")) {
let comm = comm.trim();
if !comm.is_empty() {
return comm.to_string();
}
}
format!("pid {pid}")
}
}
fn map_throttle(bits: NvmlThrottle) -> ThrottleReasons {
let categorized = NvmlThrottle::SW_POWER_CAP
| NvmlThrottle::SW_THERMAL_SLOWDOWN
| NvmlThrottle::HW_THERMAL_SLOWDOWN
| NvmlThrottle::HW_SLOWDOWN
| NvmlThrottle::HW_POWER_BRAKE_SLOWDOWN
| NvmlThrottle::SYNC_BOOST
| NvmlThrottle::APPLICATIONS_CLOCKS_SETTING
| NvmlThrottle::DISPLAY_CLOCK_SETTING
| NvmlThrottle::GPU_IDLE
| NvmlThrottle::NONE;
let other = bits.intersects(
NvmlThrottle::APPLICATIONS_CLOCKS_SETTING | NvmlThrottle::DISPLAY_CLOCK_SETTING,
) || !(bits - categorized).is_empty();
ThrottleReasons {
thermal: bits
.intersects(NvmlThrottle::SW_THERMAL_SLOWDOWN | NvmlThrottle::HW_THERMAL_SLOWDOWN),
power_cap: bits.contains(NvmlThrottle::SW_POWER_CAP),
hw_slowdown: bits
.intersects(NvmlThrottle::HW_SLOWDOWN | NvmlThrottle::HW_POWER_BRAKE_SLOWDOWN),
sync_boost: bits.contains(NvmlThrottle::SYNC_BOOST),
other,
}
}
impl GpuBackend for NvidiaBackend {
fn name(&self) -> &'static str {
"nvidia"
}
fn devices(&mut self) -> Vec<DeviceId> {
self.devs.iter().map(|(_, id)| id.clone()).collect()
}
fn static_info(&mut self, dev: &DeviceId) -> Result<StaticInfo, BackendError> {
let i = self.index_of(dev)?;
let d = self
.nvml
.device_by_index(i)
.map_err(|e| BackendError::Unavailable(e.to_string()))?;
#[cfg(target_os = "linux")]
let process_hint = self.process_hint.clone();
#[cfg(target_os = "linux")]
let source_caveat = None;
#[cfg(target_os = "windows")]
let (process_hint, source_caveat) = {
#[cfg(feature = "wddm")]
let pdh = if !crate::wddm::pdh::shared().engine_object_present() {
PdhAttribution::NoPdh
} else if self.luid_of(dev).is_some() {
PdhAttribution::Available
} else {
PdhAttribution::NoLuidMatch
};
#[cfg(not(feature = "wddm"))]
let pdh = PdhAttribution::NoPdh;
(
windows_process_hint(device_driver_model(&d), pdh),
windows_source_caveat(pdh),
)
};
Ok(StaticInfo {
id: dev.clone(),
vendor: Vendor::Nvidia,
name: opt(d.name()).unwrap_or_else(|| "NVIDIA GPU".into()),
backend: "nvidia".into(),
mem_total_bytes: opt(d.memory_info()).map(|m| m.total),
power_limit_mw: opt(d.enforced_power_limit()),
max_sm_clock_mhz: opt(d.max_clock_info(Clock::SM)),
temp_slowdown_c: opt(d.temperature_threshold(TemperatureThreshold::Slowdown))
.map(|t| t as f32),
driver_version: opt(self.nvml.sys_driver_version()),
process_hint,
source_caveat,
})
}
fn refresh_dynamic(&mut self, dev: &DeviceId) -> Result<DynamicSample, BackendError> {
let i = self.index_of(dev)?;
let d = self
.nvml
.device_by_index(i)
.map_err(|e| BackendError::Unavailable(e.to_string()))?;
let util = opt(d.utilization_rates());
let nvml_mem_used = opt(d.memory_info()).map(|m| m.used);
#[cfg(all(target_os = "windows", feature = "wddm"))]
let mem_used_bytes = self
.luid_of(dev)
.and_then(|(h, l)| {
crate::wddm::pdh::shared()
.snapshot(now_ms())
.and_then(|s| crate::wddm::pdh::adapter_bytes(&s.adapter_dedicated, h, l))
})
.or(nvml_mem_used);
#[cfg(not(all(target_os = "windows", feature = "wddm")))]
let mem_used_bytes = nvml_mem_used;
let throttle = opt(d.current_throttle_reasons()).map(map_throttle);
Ok(DynamicSample {
ts_ms: now_ms(),
util_pct: util.as_ref().map(|u| u.gpu as f32),
util_engine: None,
mem_used_bytes,
power_mw: opt(d.power_usage()),
temp_c: opt(d.temperature(TemperatureSensor::Gpu)).map(|t| t as f32),
fan_pct: opt(d.fan_speed(0)).map(|f| f as f32),
sm_clock_mhz: opt(d.clock_info(Clock::SM)),
mem_clock_mhz: opt(d.clock_info(Clock::Memory)),
encoder_pct: opt(d.encoder_utilization()).map(|e| e.utilization as f32),
decoder_pct: opt(d.decoder_utilization()).map(|e| e.utilization as f32),
throttle,
})
}
fn refresh_processes(&mut self, dev: &DeviceId) -> Result<Vec<ProcessSample>, BackendError> {
let i = self.index_of(dev)?;
let d = self
.nvml
.device_by_index(i)
.map_err(|e| BackendError::Unavailable(e.to_string()))?;
#[cfg(target_os = "linux")]
let (compute, graphics) = (
d.running_compute_processes().unwrap_or_default(),
d.running_graphics_processes().unwrap_or_default(),
);
#[cfg(target_os = "windows")]
let (compute, graphics) = (
match d.running_compute_processes() {
Ok(v) => v,
Err(e) if should_retry_v2(&e) => {
d.running_compute_processes_v2().unwrap_or_default()
}
Err(_) => Vec::new(),
},
match d.running_graphics_processes() {
Ok(v) => v,
Err(e) if should_retry_v2(&e) => {
d.running_graphics_processes_v2().unwrap_or_default()
}
Err(_) => Vec::new(),
},
);
let mut by_pid: HashMap<u32, ProcessSample> = HashMap::new();
for (list, kind) in [
(compute, ProcessKind::Compute),
(graphics, ProcessKind::Graphics),
] {
for p in list {
let mem = used_gpu_memory_bytes(&p.used_gpu_memory);
by_pid
.entry(p.pid)
.and_modify(|e| {
e.kind = ProcessKind::Both;
if e.mem_bytes.is_none() {
e.mem_bytes = mem;
}
})
.or_insert_with(|| ProcessSample {
pid: p.pid,
name: self.process_name(p.pid),
kind,
mem_bytes: mem,
util_pct: None,
cpu_pct: None,
container: None,
});
}
}
#[cfg(target_os = "linux")]
{
let since = self.last_util_ts.get(&i).copied().unwrap_or(0);
if let Ok(samples) = d.process_utilization_stats(since) {
let mut newest = since;
for s in samples {
newest = newest.max(s.timestamp);
if let Some(p) = by_pid.get_mut(&s.pid) {
p.util_pct = Some(s.sm_util as f32);
}
}
self.last_util_ts.insert(i, newest);
}
}
#[cfg(target_os = "linux")]
{
let live: Vec<u32> = by_pid.keys().copied().collect();
for (pid, p) in by_pid.iter_mut() {
p.cpu_pct = self.cpu.sample(*pid);
p.container = crate::proc_meta::container_of(*pid);
}
self.cpu.prune(&live);
}
#[cfg(all(target_os = "windows", feature = "wddm"))]
if let Some((h, l)) = self.luid_of(dev) {
if let Some(snap) = crate::wddm::pdh::shared().snapshot(now_ms()) {
let util = crate::wddm::pdh::per_pid_util(&snap.engine_util, h, l);
let mem = crate::wddm::pdh::per_pid_bytes(&snap.proc_dedicated, h, l);
for (pid, p) in by_pid.iter_mut() {
if p.mem_bytes.is_none() {
p.mem_bytes = mem.get(pid).copied();
}
if p.util_pct.is_none() {
p.util_pct = util.get(pid).map(|u| u.pct as f32);
}
}
let mut pdh_only: Vec<u32> = util
.keys()
.chain(mem.keys())
.copied()
.filter(|pid| !by_pid.contains_key(pid))
.collect();
pdh_only.sort_unstable();
pdh_only.dedup();
for pid in pdh_only {
by_pid.insert(
pid,
ProcessSample {
pid,
name: crate::wddm::os_process_name(pid),
kind: if util.get(&pid).is_some_and(|u| u.compute_hint) {
ProcessKind::Compute
} else {
ProcessKind::Unknown
},
mem_bytes: mem.get(&pid).copied(),
util_pct: util.get(&pid).map(|u| u.pct as f32),
cpu_pct: None,
container: None,
},
);
}
}
}
Ok(by_pid.into_values().collect())
}
}
#[cfg(test)]
mod tests {
use super::{
classify_driver_model_err, explicit_lib_path_for, is_wsl, map_throttle, should_retry_v2,
used_gpu_memory_bytes, windows_process_hint, windows_source_caveat, DriverModelClass,
NvmlError, NvmlThrottle, PdhAttribution, UsedGpuMemory,
};
#[test]
fn is_wsl_matches_real_kernel_release_strings() {
assert!(is_wsl("5.15.167.4-microsoft-standard-WSL2"));
assert!(is_wsl("4.4.0-19041-Microsoft"));
assert!(!is_wsl("6.17.0-35-generic"));
assert!(!is_wsl(""));
}
#[test]
fn throttle_sw_power_cap_maps_to_power_cap() {
let r = map_throttle(NvmlThrottle::SW_POWER_CAP);
assert!(r.power_cap);
assert!(!r.thermal && !r.hw_slowdown && !r.sync_boost && !r.other);
}
#[test]
fn throttle_both_thermal_tiers_map_to_thermal() {
assert!(map_throttle(NvmlThrottle::SW_THERMAL_SLOWDOWN).thermal);
assert!(map_throttle(NvmlThrottle::HW_THERMAL_SLOWDOWN).thermal);
let both =
map_throttle(NvmlThrottle::SW_THERMAL_SLOWDOWN | NvmlThrottle::HW_THERMAL_SLOWDOWN);
assert!(both.thermal && !both.power_cap && !both.hw_slowdown);
}
#[test]
fn throttle_hw_slowdown_and_power_brake_map_to_hw_slowdown() {
assert!(map_throttle(NvmlThrottle::HW_SLOWDOWN).hw_slowdown);
assert!(map_throttle(NvmlThrottle::HW_POWER_BRAKE_SLOWDOWN).hw_slowdown);
let both = map_throttle(NvmlThrottle::HW_SLOWDOWN | NvmlThrottle::HW_POWER_BRAKE_SLOWDOWN);
assert!(both.hw_slowdown && !both.thermal && !both.power_cap);
}
#[test]
fn throttle_sync_boost_maps_to_sync_boost() {
let r = map_throttle(NvmlThrottle::SYNC_BOOST);
assert!(r.sync_boost);
assert!(!r.thermal && !r.power_cap && !r.hw_slowdown && !r.other);
}
#[test]
fn throttle_clock_config_bits_map_to_other() {
let app = map_throttle(NvmlThrottle::APPLICATIONS_CLOCKS_SETTING);
assert!(app.other);
assert!(!app.thermal && !app.power_cap && !app.hw_slowdown && !app.sync_boost);
let disp = map_throttle(NvmlThrottle::DISPLAY_CLOCK_SETTING);
assert!(disp.other);
assert!(!disp.thermal && !disp.power_cap && !disp.hw_slowdown && !disp.sync_boost);
}
#[test]
fn throttle_unknown_future_bit_maps_to_other() {
let future = NvmlThrottle::from_bits_retain(1 << 40);
let r = map_throttle(future);
assert!(
r.other,
"unrecognized bit must surface as other, not vanish"
);
assert!(!r.thermal && !r.power_cap && !r.hw_slowdown && !r.sync_boost);
}
#[test]
fn throttle_idle_and_none_set_nothing() {
assert!(!map_throttle(NvmlThrottle::GPU_IDLE).any());
assert!(!map_throttle(NvmlThrottle::NONE).any());
assert!(!map_throttle(NvmlThrottle::empty()).any());
let mixed = map_throttle(NvmlThrottle::GPU_IDLE | NvmlThrottle::SW_POWER_CAP);
assert!(mixed.power_cap);
}
#[test]
fn throttle_combined_reasons_set_all_relevant() {
let r = map_throttle(NvmlThrottle::SW_POWER_CAP | NvmlThrottle::HW_THERMAL_SLOWDOWN);
assert!(r.power_cap && r.thermal);
assert!(!r.hw_slowdown && !r.sync_boost && !r.other);
}
#[test]
fn lib_path_linux_tries_versioned_soname_first() {
assert_eq!(explicit_lib_path_for("linux"), Some("libnvidia-ml.so.1"));
}
#[test]
fn lib_path_windows_relies_on_system32_default_search() {
assert_eq!(explicit_lib_path_for("windows"), None);
}
#[test]
fn v2_process_list_retry_only_on_missing_v3_symbol() {
assert!(should_retry_v2(&NvmlError::FailedToLoadSymbol(
"nvmlDeviceGetComputeRunningProcesses_v3".into()
)));
assert!(!should_retry_v2(&NvmlError::NotSupported));
assert!(!should_retry_v2(&NvmlError::DriverNotLoaded));
assert!(!should_retry_v2(&NvmlError::GpuLost));
assert!(!should_retry_v2(&NvmlError::Unknown));
assert!(!should_retry_v2(&NvmlError::UnexpectedVariant(2)));
}
#[test]
fn wddm_per_process_memory_unavailable_is_none_never_zero() {
assert_eq!(used_gpu_memory_bytes(&UsedGpuMemory::Unavailable), None);
assert_eq!(
used_gpu_memory_bytes(&UsedGpuMemory::Used(123_456_789)),
Some(123_456_789)
);
}
#[test]
fn driver_model_hint_wddm_names_only_a_source_that_delivers() {
let hint = windows_process_hint(DriverModelClass::Wddm, PdhAttribution::Available)
.expect("WDDM must come with an explanation");
assert!(
hint.contains("WDDM") && hint.contains("NVML"),
"hint must name the driver model and why NVML is not the source: {hint}"
);
let hint = windows_process_hint(DriverModelClass::Wddm, PdhAttribution::NoLuidMatch)
.expect("a failed match must be explained");
assert!(
hint.contains("unavailable") && hint.contains("match failed"),
"failed match must read as unavailable, not as a working source: {hint}"
);
let hint = windows_process_hint(DriverModelClass::Wddm, PdhAttribution::NoPdh)
.expect("missing PDH must be explained");
assert!(hint.contains("unavailable"), "{hint}");
}
#[test]
fn driver_model_hint_tcc_has_nothing_to_explain() {
assert_eq!(
windows_process_hint(DriverModelClass::Tcc, PdhAttribution::Available),
None
);
assert_eq!(
windows_process_hint(DriverModelClass::Tcc, PdhAttribution::NoPdh),
None
);
}
#[test]
fn driver_model_hint_unknown_model_is_flagged_not_fatal() {
let hint =
windows_process_hint(DriverModelClass::UnknownVariant, PdhAttribution::Available)
.expect("an unknown driver model must be flagged");
assert!(
hint.contains("unknown"),
"hint must say the model is unknown: {hint}"
);
}
#[test]
fn windows_source_caveat_names_the_real_memory_source() {
let c = windows_source_caveat(PdhAttribution::Available).unwrap();
assert!(c.contains("VidMm") && c.contains("PDH"), "{c}");
for pdh in [PdhAttribution::NoLuidMatch, PdhAttribution::NoPdh] {
let c = windows_source_caveat(pdh).unwrap();
assert!(
c.contains("driver's view") && !c.contains("PDH"),
"fallback caveat must label the driver view, not PDH: {c}"
);
}
}
#[test]
fn driver_model_classification_handles_mcdm_and_query_failure() {
assert_eq!(
classify_driver_model_err(&NvmlError::UnexpectedVariant(2)),
DriverModelClass::UnknownVariant
);
assert_eq!(
classify_driver_model_err(&NvmlError::NotSupported),
DriverModelClass::Wddm
);
assert_eq!(
classify_driver_model_err(&NvmlError::Unknown),
DriverModelClass::Wddm
);
}
}