use std::collections::{HashMap, HashSet};
use std::sync::{Mutex, OnceLock};
use unicode_normalization::UnicodeNormalization;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessInfo {
pub pid: u32,
pub used_gpu_memory: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UtilSample {
pub pid: u32,
pub sm_util: u32,
pub mem_util: u32,
pub time_stamp: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemInfo {
pub used_bytes: u64,
pub total_bytes: u64,
}
fn warned_modes() -> &'static Mutex<HashSet<String>> {
static SLOT: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(HashSet::new()))
}
fn warn_once(mode: &str, message: &str) {
let mut set = match warned_modes().lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if !set.insert(mode.to_string()) {
return;
}
eprintln!("[dexcost][gpu] {}: {}", mode, message);
}
#[doc(hidden)]
pub fn reset_warning_state_for_tests() {
let mut set = match warned_modes().lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
set.clear();
}
pub fn normalize_product_name(raw: &str) -> String {
let nfc: String = raw.nfc().collect();
let collapsed: String = nfc
.split(|c: char| c.is_whitespace())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
collapsed.to_lowercase()
}
#[cfg(feature = "gpu")]
mod nvml_real {
use super::*;
use nvml_wrapper::Nvml;
use std::sync::OnceLock;
static NVML: OnceLock<Option<Nvml>> = OnceLock::new();
pub fn nvml_available() -> bool {
true
}
fn ensure_init() -> Option<&'static Nvml> {
NVML.get_or_init(|| match Nvml::init() {
Ok(n) => Some(n),
Err(e) => {
warn_once(
"gpu_nvml_init_failed",
&format!("NVML init failed ({}); GPU capture disabled", e),
);
None
}
})
.as_ref()
}
pub fn init_nvml() -> bool {
ensure_init().is_some()
}
pub fn shutdown_nvml() {
}
pub fn get_device_count() -> Option<u32> {
let n = ensure_init()?;
match n.device_count() {
Ok(c) => Some(c),
Err(e) => {
warn_once(
"gpu_device_count_failed",
&format!("nvmlDeviceGetCount failed ({})", e),
);
None
}
}
}
}
#[cfg(not(feature = "gpu"))]
mod nvml_real {
use super::*;
pub fn nvml_available() -> bool {
false
}
pub fn init_nvml() -> bool {
warn_once(
"gpu_nvml_not_linked",
"nvml-wrapper crate not linked; GPU capture disabled. \
Rebuild with --features gpu to enable.",
);
false
}
pub fn shutdown_nvml() {}
pub fn get_device_count() -> Option<u32> {
None
}
}
pub use nvml_real::{get_device_count, init_nvml, nvml_available, shutdown_nvml};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DeviceHandle(pub u32);
pub fn get_device_handle(index: u32) -> Option<DeviceHandle> {
if !nvml_available() {
return None;
}
Some(DeviceHandle(index))
}
pub fn get_product_name(handle: DeviceHandle) -> Option<String> {
real::get_product_name(handle)
}
pub fn get_compute_running_processes(handle: DeviceHandle) -> Option<Vec<ProcessInfo>> {
real::get_compute_running_processes(handle)
}
pub fn get_process_utilization(
handle: DeviceHandle,
last_seen_timestamps: &mut HashMap<u32, u64>,
) -> Option<HashMap<u32, UtilSample>> {
real::get_process_utilization(handle, last_seen_timestamps)
}
pub fn get_memory_info(handle: DeviceHandle) -> Option<MemInfo> {
real::get_memory_info(handle)
}
pub fn get_mig_mode(handle: DeviceHandle) -> bool {
real::get_mig_mode(handle)
}
#[cfg(feature = "gpu")]
mod real {
use super::*;
use nvml_wrapper::Nvml;
use std::sync::OnceLock;
static NVML: OnceLock<Option<Nvml>> = OnceLock::new();
fn ensure() -> Option<&'static Nvml> {
NVML.get_or_init(|| Nvml::init().ok()).as_ref()
}
pub fn get_product_name(handle: DeviceHandle) -> Option<String> {
let n = ensure()?;
let d = match n.device_by_index(handle.0) {
Ok(d) => d,
Err(_) => return None,
};
match d.name() {
Ok(s) => Some(normalize_product_name(&s)),
Err(e) => {
warn_once(
"gpu_product_name_failed",
&format!("nvmlDeviceGetName failed ({})", e),
);
None
}
}
}
pub fn get_compute_running_processes(handle: DeviceHandle) -> Option<Vec<ProcessInfo>> {
let n = ensure()?;
let d = match n.device_by_index(handle.0) {
Ok(d) => d,
Err(_) => return None,
};
match d.running_compute_processes() {
Ok(procs) => Some(
procs
.into_iter()
.map(|p| ProcessInfo {
pid: p.pid,
used_gpu_memory: match p.used_gpu_memory {
nvml_wrapper::enums::device::UsedGpuMemory::Used(b) => b,
_ => 0,
},
})
.collect(),
),
Err(e) => {
warn_once(
"gpu_nvml_permission_denied",
&format!(
"nvmlDeviceGetComputeRunningProcesses failed ({}); \
GpuAccountant will degrade to self-PID-only",
e
),
);
None
}
}
}
pub fn get_process_utilization(
handle: DeviceHandle,
last_seen_timestamps: &mut HashMap<u32, u64>,
) -> Option<HashMap<u32, UtilSample>> {
let n = ensure()?;
let d = match n.device_by_index(handle.0) {
Ok(d) => d,
Err(_) => return None,
};
let base_ts = last_seen_timestamps.values().copied().min().unwrap_or(0);
match d.process_utilization_stats(Some(base_ts)) {
Ok(samples) => {
let mut out = HashMap::new();
for s in samples {
let pid = s.pid;
let ts = s.timestamp;
out.insert(
pid,
UtilSample {
pid,
sm_util: s.sm_util,
mem_util: s.mem_util,
time_stamp: ts,
},
);
last_seen_timestamps.insert(pid, ts);
}
Some(out)
}
Err(e) => {
warn_once(
"gpu_process_utilization_failed",
&format!("nvmlDeviceGetProcessUtilization failed ({})", e),
);
None
}
}
}
pub fn get_memory_info(handle: DeviceHandle) -> Option<MemInfo> {
let n = ensure()?;
let d = match n.device_by_index(handle.0) {
Ok(d) => d,
Err(_) => return None,
};
match d.memory_info() {
Ok(m) => Some(MemInfo {
used_bytes: m.used,
total_bytes: m.total,
}),
Err(_) => None,
}
}
pub fn get_mig_mode(handle: DeviceHandle) -> bool {
let n = match ensure() {
Some(n) => n,
None => return false,
};
let d = match n.device_by_index(handle.0) {
Ok(d) => d,
Err(_) => return false,
};
match d.mig_mode() {
Ok(m) => matches!(m.current, nvml_wrapper::enum_wrappers::device::MigMode::Enabled),
Err(_) => false,
}
}
}
#[cfg(not(feature = "gpu"))]
mod real {
use super::*;
pub fn get_product_name(_handle: DeviceHandle) -> Option<String> {
None
}
pub fn get_compute_running_processes(_handle: DeviceHandle) -> Option<Vec<ProcessInfo>> {
None
}
pub fn get_process_utilization(
_handle: DeviceHandle,
_last_seen_timestamps: &mut HashMap<u32, u64>,
) -> Option<HashMap<u32, UtilSample>> {
None
}
pub fn get_memory_info(_handle: DeviceHandle) -> Option<MemInfo> {
None
}
pub fn get_mig_mode(_handle: DeviceHandle) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_product_name_collapses_nbsp() {
let raw = "NVIDIA\u{00A0}H100\u{00A0}80GB HBM3";
let n = normalize_product_name(raw);
assert_eq!(n, "nvidia h100 80gb hbm3");
}
#[test]
fn normalize_product_name_collapses_nnbsp_and_zwsp() {
let raw = "NVIDIA\u{202F}A100 40GB";
let n = normalize_product_name(raw);
assert_eq!(n, "nvidia a100 40gb");
}
#[test]
fn normalize_product_name_lowercases() {
assert_eq!(normalize_product_name("NVIDIA H100"), "nvidia h100");
}
#[test]
fn normalize_product_name_nfc() {
let composed = "café";
let decomposed = "cafe\u{0301}";
assert_eq!(
normalize_product_name(composed),
normalize_product_name(decomposed)
);
}
#[test]
fn no_feature_returns_no_devices() {
#[cfg(not(feature = "gpu"))]
{
assert!(!nvml_available());
assert!(!init_nvml());
assert_eq!(get_device_count(), None);
assert!(get_device_handle(0).is_none());
}
}
#[test]
fn warn_once_is_log_once() {
reset_warning_state_for_tests();
warn_once("test_mode_x", "first");
warn_once("test_mode_x", "second"); let set = warned_modes().lock().unwrap();
assert!(set.contains("test_mode_x"));
}
}