use std::io;
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, Debug)]
pub struct NvLinkSnapshot {
pub gpu_index: u32,
pub gpu_name: String,
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,
];
pub fn read_all_nvlink_stats() -> io::Result<Vec<NvLinkSnapshot>> {
let nvml = match Nvml::init() {
Ok(n) => n,
Err(_) => 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_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 version = nvlink.version().ok();
let remote_device_type = read_remote_device_type(nvml, &device, link_id);
let remote_pci_bdf = nvlink.remote_pci_info().ok().map(|p| p.bus_id);
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);
let speed_gbps = if is_active {
read_link_speed_gbps(&device, link_id).or(common_speed_gbps)
} else {
None
};
if let Some(s) = speed_gbps {
total_speed_gbps += s;
}
links.push(LinkSnapshot {
link_id,
is_active,
version,
speed_gbps,
remote_device_type,
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,
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(),
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(),
link_count: 0,
link_gbps: None,
tx_bytes: None,
rx_bytes: None,
links: Vec::new(),
};
assert_eq!(snap.active_links(), 0);
}
}