use std::path::Path;
use std::process::Command;
use crate::GpuInfo;
use crate::report::{GpuSurvey, NoteKind};
use crate::vendor::{GpuArch, GpuVendor};
fn driver_present() -> bool {
Path::new("/dev/nvidiactl").exists() || Path::new("/proc/driver/nvidia").exists()
}
pub(crate) fn probe(out: &mut GpuSurvey) {
if cfg!(target_os = "linux") && !driver_present() {
return;
}
let output = match Command::new("nvidia-smi")
.args([
"--query-gpu=index,compute_cap,memory.total,name",
"--format=csv,noheader,nounits",
])
.output()
{
Ok(o) if o.status.success() => o,
Ok(o) => {
out.note(
GpuVendor::Nvidia,
NoteKind::ToolFailed,
format!(
"`nvidia-smi` exited {}: {}. An NVIDIA driver is loaded but the \
tool cannot enumerate devices, so this box reports 0 NVIDIA GPUs.",
o.status,
String::from_utf8_lossy(&o.stderr).trim(),
),
);
return;
}
Err(_) => {
if cfg!(target_os = "linux") {
out.note(
GpuVendor::Nvidia,
NoteKind::HardwareUnusable,
"an NVIDIA driver is present but `nvidia-smi` is not on PATH, so \
GPUs cannot be enumerated. Install the NVIDIA utilities package \
(or, in Docker, use a CUDA base image / the NVIDIA container \
toolkit)."
.to_string(),
);
}
return;
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().filter(|l| !l.trim().is_empty()) {
match parse_csv_row(line) {
Some(g) => out.devices.push(g),
None => out.note(
GpuVendor::Nvidia,
NoteKind::Unparsable,
format!("could not parse an `nvidia-smi` row, GPU skipped: {line:?}"),
),
}
}
}
fn parse_csv_row(line: &str) -> Option<GpuInfo> {
let parts: Vec<&str> = line.splitn(4, ", ").collect();
if parts.len() < 4 {
return None;
}
Some(GpuInfo {
index: parts[0].trim().parse().ok()?,
vendor: GpuVendor::Nvidia,
arch: GpuArch::parse(GpuVendor::Nvidia, parts[1])?,
total_memory_mb: parts[2].trim().parse().ok()?,
name: parts[3].trim().to_string(),
})
}
pub fn nvidia_driver_version() -> Option<String> {
let output = Command::new("nvidia-smi")
.args(["--query-gpu=driver_version", "--format=csv,noheader"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let s = String::from_utf8_lossy(&output.stdout);
Some(s.lines().next()?.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_well_formed_row() {
let g = parse_csv_row("1, 6.1, 6078, NVIDIA GeForce GTX 1060 6GB").unwrap();
assert_eq!(g.index, 1);
assert_eq!(g.name, "NVIDIA GeForce GTX 1060 6GB");
assert_eq!(g.arch, GpuArch::Sm { major: 6, minor: 1 });
assert_eq!(g.vendor, GpuVendor::Nvidia);
assert_eq!(g.total_memory_mb, 6078);
}
#[test]
fn keeps_a_comma_inside_the_name() {
let g = parse_csv_row("0, 8.0, 81920, NVIDIA A100, 80GB").unwrap();
assert_eq!(g.name, "NVIDIA A100, 80GB");
assert_eq!(g.total_memory_mb, 81920);
}
#[test]
fn rejects_malformed_rows() {
assert!(parse_csv_row("0, 8.9, three").is_none());
assert!(parse_csv_row("x, 8.9, 24564, name").is_none());
assert!(parse_csv_row("0, notacap, 24564, name").is_none());
}
}