use std::collections::HashMap;
use std::io;
use std::sync::{LazyLock, Mutex, OnceLock};
use nvml_wrapper::enum_wrappers::device::{Clock, TemperatureSensor};
use nvml_wrapper::enum_wrappers::nv_link::{ErrorCounter, IntDeviceType};
use nvml_wrapper::enums::device::SampleValue;
use nvml_wrapper::structs::device::FieldId;
use nvml_wrapper::Nvml;
use nvml_wrapper_sys::bindings::field_id::{
NVML_FI_DEV_NVLINK_LINK_COUNT, NVML_FI_DEV_NVLINK_SPEED_MBPS_COMMON,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L0, NVML_FI_DEV_NVLINK_SPEED_MBPS_L1,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L10, NVML_FI_DEV_NVLINK_SPEED_MBPS_L11,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L2, NVML_FI_DEV_NVLINK_SPEED_MBPS_L3,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L4, NVML_FI_DEV_NVLINK_SPEED_MBPS_L5,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L6, NVML_FI_DEV_NVLINK_SPEED_MBPS_L7,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L8, NVML_FI_DEV_NVLINK_SPEED_MBPS_L9,
NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_RX, NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_TX,
};
use nvml_wrapper_sys::bindings::{
nvmlFieldValue_st, nvmlValueType_enum_NVML_VALUE_TYPE_UNSIGNED_LONG_LONG, nvmlValue_t,
NVML_NVLINK_MAX_LINKS,
};
#[derive(Clone, Debug, PartialEq)]
pub enum RemoteDeviceType {
Gpu,
IbmNpu,
Switch,
Unknown,
}
impl RemoteDeviceType {
pub fn label(&self) -> &'static str {
match self {
RemoteDeviceType::Gpu => "GPU",
RemoteDeviceType::IbmNpu => "NPU",
RemoteDeviceType::Switch => "NVSwitch",
RemoteDeviceType::Unknown => "?",
}
}
}
impl From<IntDeviceType> for RemoteDeviceType {
fn from(value: IntDeviceType) -> Self {
match value {
IntDeviceType::Gpu => RemoteDeviceType::Gpu,
IntDeviceType::Ibmnpu => RemoteDeviceType::IbmNpu,
IntDeviceType::Switch => RemoteDeviceType::Switch,
IntDeviceType::Unknown => RemoteDeviceType::Unknown,
}
}
}
#[derive(Clone, Debug)]
pub struct LinkSnapshot {
pub link_id: u32,
pub is_active: bool,
pub version: Option<u32>,
#[allow(dead_code)]
pub speed_gbps: Option<f64>,
pub remote_device_type: RemoteDeviceType,
pub remote_pci_bdf: Option<String>,
pub tx_bytes: Option<u64>,
pub rx_bytes: Option<u64>,
pub crc_error_count: Option<u64>,
pub replay_error_count: Option<u64>,
pub recovery_error_count: Option<u64>,
}
#[derive(Clone)]
struct LinkMeta {
version: Option<u32>,
remote_device_type: RemoteDeviceType,
remote_pci_bdf: Option<String>,
speed_gbps: Option<f64>,
}
fn link_meta(
nvml: &Nvml,
device: &nvml_wrapper::Device<'_>,
gpu: u32,
link_id: u32,
common_speed_gbps: Option<f64>,
) -> LinkMeta {
static CACHE: LazyLock<Mutex<HashMap<(u32, u32), LinkMeta>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some(meta) = cache.get(&(gpu, link_id)) {
return meta.clone();
}
let nvlink = device.link_wrapper_for(link_id);
let meta = LinkMeta {
version: nvlink.version().ok(),
remote_device_type: read_remote_device_type(nvml, device, link_id),
remote_pci_bdf: nvlink.remote_pci_info().ok().map(|p| p.bus_id),
speed_gbps: read_link_speed_gbps(device, link_id).or(common_speed_gbps),
};
if meta.version.is_some()
&& meta.remote_device_type != RemoteDeviceType::Unknown
&& meta.speed_gbps.is_some()
{
cache.insert((gpu, link_id), meta.clone());
}
meta
}
#[derive(Clone, Debug)]
pub struct NvLinkSnapshot {
pub gpu_index: u32,
pub gpu_name: String,
pub metrics: Option<crate::gpu::GpuMetrics>,
pub link_count: u32,
pub link_gbps: Option<f64>,
pub tx_bytes: Option<u64>,
pub rx_bytes: Option<u64>,
pub links: Vec<LinkSnapshot>,
}
impl NvLinkSnapshot {
pub fn active_links(&self) -> u32 {
self.links.iter().filter(|l| l.is_active).count() as u32
}
}
const LINK_SPEED_FIELD_IDS: [u32; 12] = [
NVML_FI_DEV_NVLINK_SPEED_MBPS_L0,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L1,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L2,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L3,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L4,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L5,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L6,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L7,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L8,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L9,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L10,
NVML_FI_DEV_NVLINK_SPEED_MBPS_L11,
];
fn nvml() -> Option<&'static Nvml> {
static INSTANCE: OnceLock<Nvml> = OnceLock::new();
static INIT: Mutex<()> = Mutex::new(());
if let Some(n) = INSTANCE.get() {
return Some(n);
}
let _guard = INIT.lock().unwrap_or_else(|e| e.into_inner());
if let Some(n) = INSTANCE.get() {
return Some(n);
}
let n = Nvml::init().ok()?;
Some(INSTANCE.get_or_init(|| n))
}
pub fn read_all_nvlink_stats() -> io::Result<Vec<NvLinkSnapshot>> {
let nvml = match nvml() {
Some(n) => n,
None => return Ok(Vec::new()),
};
let device_count = match nvml.device_count() {
Ok(c) => c,
Err(_) => return Ok(Vec::new()),
};
let mut snapshots = Vec::with_capacity(device_count as usize);
for idx in 0..device_count {
if let Some(snap) = read_device_snapshot(nvml, idx) {
snapshots.push(snap);
}
}
Ok(snapshots)
}
fn read_gpu_metrics(device: &nvml_wrapper::Device<'_>) -> crate::gpu::GpuMetrics {
let mem = device.memory_info().ok();
crate::gpu::GpuMetrics {
util_pct: device.utilization_rates().ok().map(|u| u.gpu),
vram_used_mb: mem.as_ref().map(|m| m.used / (1024 * 1024)),
vram_total_mb: mem.as_ref().map(|m| m.total / (1024 * 1024)),
temp_c: device
.temperature(TemperatureSensor::Gpu)
.ok()
.map(|t| t as i64),
power_w: device.power_usage().ok().map(|mw| mw as f64 / 1000.0),
clock_mhz: device.clock_info(Clock::Graphics).ok(),
}
}
fn read_device_snapshot(nvml: &Nvml, idx: u32) -> Option<NvLinkSnapshot> {
let device = nvml.device_by_index(idx).ok()?;
let gpu_name = device.name().unwrap_or_default();
let link_count = read_link_count(&device).unwrap_or(NVML_NVLINK_MAX_LINKS);
let common_speed_gbps = read_common_speed_gbps(&device);
let mut total_speed_gbps = 0.0;
let mut links = Vec::with_capacity(link_count as usize);
for link_id in 0..link_count {
let nvlink = device.link_wrapper_for(link_id);
let is_active = nvlink.is_active().unwrap_or(false);
let meta = if is_active {
link_meta(nvml, &device, idx, link_id, common_speed_gbps)
} else {
LinkMeta {
version: None,
remote_device_type: RemoteDeviceType::Unknown,
remote_pci_bdf: None,
speed_gbps: None,
}
};
let crc_error_count = nvlink.error_counter(ErrorCounter::DlCrcFlit).ok();
let replay_error_count = nvlink.error_counter(ErrorCounter::DlReplay).ok();
let recovery_error_count = nvlink.error_counter(ErrorCounter::DlRecovery).ok();
let (tx_bytes, rx_bytes) = read_link_throughput(nvml, &device, link_id);
if let Some(s) = meta.speed_gbps {
total_speed_gbps += s;
}
links.push(LinkSnapshot {
link_id,
is_active,
version: meta.version,
speed_gbps: meta.speed_gbps,
remote_device_type: meta.remote_device_type,
remote_pci_bdf: meta.remote_pci_bdf,
tx_bytes,
rx_bytes,
crc_error_count,
replay_error_count,
recovery_error_count,
});
}
let (tx_bytes, rx_bytes) = read_link_throughput(nvml, &device, u32::MAX);
Some(NvLinkSnapshot {
gpu_index: idx,
gpu_name,
metrics: Some(read_gpu_metrics(&device)).filter(|m| !m.is_empty()),
link_count,
link_gbps: if total_speed_gbps > 0.0 {
Some(total_speed_gbps)
} else {
None
},
tx_bytes,
rx_bytes,
links,
})
}
fn make_field(field_id: u32, scope_id: u32) -> nvmlFieldValue_st {
nvmlFieldValue_st {
fieldId: field_id,
scopeId: scope_id,
timestamp: 0,
latencyUsec: 0,
valueType: 0,
nvmlReturn: 0,
value: nvmlValue_t { ullVal: 0 },
}
}
fn read_field_u32(device: &nvml_wrapper::Device<'_>, field_id: u32) -> Option<u32> {
let results = device.field_values_for(&[FieldId(field_id)]).ok()?;
let sample = results.into_iter().next()?.ok()?;
match sample.value.ok()? {
SampleValue::U32(v) => Some(v),
_ => None,
}
}
fn read_link_count(device: &nvml_wrapper::Device<'_>) -> Option<u32> {
read_field_u32(device, NVML_FI_DEV_NVLINK_LINK_COUNT)
}
fn read_common_speed_gbps(device: &nvml_wrapper::Device<'_>) -> Option<f64> {
let mbps = read_field_u32(device, NVML_FI_DEV_NVLINK_SPEED_MBPS_COMMON)?;
if mbps == 0 {
None
} else {
Some(mbps as f64 / 1000.0)
}
}
fn read_link_speed_gbps(device: &nvml_wrapper::Device<'_>, link: u32) -> Option<f64> {
let field_id = *LINK_SPEED_FIELD_IDS.get(link as usize)?;
let mbps = read_field_u32(device, field_id)?;
if mbps == 0 {
None
} else {
Some(mbps as f64 / 1000.0)
}
}
fn read_link_throughput(
nvml: &Nvml,
device: &nvml_wrapper::Device<'_>,
link: u32,
) -> (Option<u64>, Option<u64>) {
let mut tx = make_field(NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_TX, link);
let mut rx = make_field(NVML_FI_DEV_NVLINK_THROUGHPUT_DATA_RX, link);
let mut fields = [tx, rx];
let rc = unsafe {
nvml.lib().nvmlDeviceGetFieldValues(
device.handle(),
fields.len() as std::os::raw::c_int,
fields.as_mut_ptr(),
)
};
if rc != 0 {
return (None, None);
}
tx = fields[0];
rx = fields[1];
let tx_bytes = if tx.nvmlReturn == 0
&& tx.valueType == nvmlValueType_enum_NVML_VALUE_TYPE_UNSIGNED_LONG_LONG
{
Some(unsafe { tx.value.ullVal }.wrapping_mul(1024))
} else {
None
};
let rx_bytes = if rx.nvmlReturn == 0
&& rx.valueType == nvmlValueType_enum_NVML_VALUE_TYPE_UNSIGNED_LONG_LONG
{
Some(unsafe { rx.value.ullVal }.wrapping_mul(1024))
} else {
None
};
(tx_bytes, rx_bytes)
}
#[allow(non_upper_case_globals)]
fn read_remote_device_type(
nvml: &Nvml,
device: &nvml_wrapper::Device<'_>,
link: u32,
) -> RemoteDeviceType {
use nvml_wrapper_sys::bindings::{
nvmlIntNvLinkDeviceType_enum_NVML_NVLINK_DEVICE_TYPE_GPU,
nvmlIntNvLinkDeviceType_enum_NVML_NVLINK_DEVICE_TYPE_IBMNPU,
nvmlIntNvLinkDeviceType_enum_NVML_NVLINK_DEVICE_TYPE_SWITCH, nvmlIntNvLinkDeviceType_t,
};
let mut dev_type: nvmlIntNvLinkDeviceType_t = 255;
let rc = unsafe {
nvml.lib()
.nvmlDeviceGetNvLinkRemoteDeviceType(device.handle(), link, &mut dev_type)
};
if rc == 0 {
match dev_type {
nvmlIntNvLinkDeviceType_enum_NVML_NVLINK_DEVICE_TYPE_GPU => RemoteDeviceType::Gpu,
nvmlIntNvLinkDeviceType_enum_NVML_NVLINK_DEVICE_TYPE_IBMNPU => RemoteDeviceType::IbmNpu,
nvmlIntNvLinkDeviceType_enum_NVML_NVLINK_DEVICE_TYPE_SWITCH => RemoteDeviceType::Switch,
_ => RemoteDeviceType::Unknown,
}
} else {
RemoteDeviceType::Unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remote_device_type_label() {
assert_eq!(RemoteDeviceType::Gpu.label(), "GPU");
assert_eq!(RemoteDeviceType::IbmNpu.label(), "NPU");
assert_eq!(RemoteDeviceType::Switch.label(), "NVSwitch");
assert_eq!(RemoteDeviceType::Unknown.label(), "?");
}
#[test]
fn remote_device_type_from_int_device_type() {
assert_eq!(
RemoteDeviceType::from(IntDeviceType::Gpu),
RemoteDeviceType::Gpu
);
assert_eq!(
RemoteDeviceType::from(IntDeviceType::Ibmnpu),
RemoteDeviceType::IbmNpu
);
assert_eq!(
RemoteDeviceType::from(IntDeviceType::Switch),
RemoteDeviceType::Switch
);
assert_eq!(
RemoteDeviceType::from(IntDeviceType::Unknown),
RemoteDeviceType::Unknown
);
}
#[test]
fn active_links_counts_active_only() {
let snap = NvLinkSnapshot {
gpu_index: 0,
gpu_name: "test".to_string(),
metrics: None,
link_count: 4,
link_gbps: None,
tx_bytes: None,
rx_bytes: None,
links: vec![
LinkSnapshot {
link_id: 0,
is_active: true,
version: None,
speed_gbps: None,
remote_device_type: RemoteDeviceType::Gpu,
remote_pci_bdf: None,
tx_bytes: None,
rx_bytes: None,
crc_error_count: None,
replay_error_count: None,
recovery_error_count: None,
},
LinkSnapshot {
link_id: 1,
is_active: false,
version: None,
speed_gbps: None,
remote_device_type: RemoteDeviceType::Unknown,
remote_pci_bdf: None,
tx_bytes: None,
rx_bytes: None,
crc_error_count: None,
replay_error_count: None,
recovery_error_count: None,
},
LinkSnapshot {
link_id: 2,
is_active: true,
version: None,
speed_gbps: None,
remote_device_type: RemoteDeviceType::Switch,
remote_pci_bdf: None,
tx_bytes: None,
rx_bytes: None,
crc_error_count: None,
replay_error_count: None,
recovery_error_count: None,
},
],
};
assert_eq!(snap.active_links(), 2);
}
#[test]
fn active_links_no_links() {
let snap = NvLinkSnapshot {
gpu_index: 0,
gpu_name: "test".to_string(),
metrics: None,
link_count: 0,
link_gbps: None,
tx_bytes: None,
rx_bytes: None,
links: Vec::new(),
};
assert_eq!(snap.active_links(), 0);
}
}