1use std::path::Path;
4use std::process::Command;
5
6use crate::GpuInfo;
7use crate::report::{GpuSurvey, NoteKind};
8use crate::vendor::{GpuArch, GpuVendor};
9
10fn driver_present() -> bool {
18 Path::new("/dev/nvidiactl").exists() || Path::new("/proc/driver/nvidia").exists()
19}
20
21pub(crate) fn probe(out: &mut GpuSurvey) {
27 if cfg!(target_os = "linux") && !driver_present() {
30 return;
31 }
32
33 let output = match Command::new("nvidia-smi")
34 .args([
40 "--query-gpu=index,compute_cap,memory.total,name",
41 "--format=csv,noheader,nounits",
42 ])
43 .output()
44 {
45 Ok(o) if o.status.success() => o,
46 Ok(o) => {
51 out.note(
52 GpuVendor::Nvidia,
53 NoteKind::ToolFailed,
54 format!(
55 "`nvidia-smi` exited {}: {}. An NVIDIA driver is loaded but the \
56 tool cannot enumerate devices, so this box reports 0 NVIDIA GPUs.",
57 o.status,
58 String::from_utf8_lossy(&o.stderr).trim(),
59 ),
60 );
61 return;
62 }
63 Err(_) => {
64 if cfg!(target_os = "linux") {
68 out.note(
69 GpuVendor::Nvidia,
70 NoteKind::HardwareUnusable,
71 "an NVIDIA driver is present but `nvidia-smi` is not on PATH, so \
72 GPUs cannot be enumerated. Install the NVIDIA utilities package \
73 (or, in Docker, use a CUDA base image / the NVIDIA container \
74 toolkit)."
75 .to_string(),
76 );
77 }
78 return;
79 }
80 };
81
82 let stdout = String::from_utf8_lossy(&output.stdout);
83 for line in stdout.lines().filter(|l| !l.trim().is_empty()) {
84 match parse_csv_row(line) {
85 Some(g) => out.devices.push(g),
86 None => out.note(
88 GpuVendor::Nvidia,
89 NoteKind::Unparsable,
90 format!("could not parse an `nvidia-smi` row, GPU skipped: {line:?}"),
91 ),
92 }
93 }
94}
95
96fn parse_csv_row(line: &str) -> Option<GpuInfo> {
99 let parts: Vec<&str> = line.splitn(4, ", ").collect();
100 if parts.len() < 4 {
101 return None;
102 }
103 Some(GpuInfo {
104 index: parts[0].trim().parse().ok()?,
105 vendor: GpuVendor::Nvidia,
106 arch: GpuArch::parse(GpuVendor::Nvidia, parts[1])?,
107 total_memory_mb: parts[2].trim().parse().ok()?,
108 name: parts[3].trim().to_string(),
109 })
110}
111
112pub fn nvidia_driver_version() -> Option<String> {
115 let output = Command::new("nvidia-smi")
116 .args(["--query-gpu=driver_version", "--format=csv,noheader"])
117 .output()
118 .ok()?;
119 if !output.status.success() {
120 return None;
121 }
122 let s = String::from_utf8_lossy(&output.stdout);
123 Some(s.lines().next()?.trim().to_string())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn parses_a_well_formed_row() {
132 let g = parse_csv_row("1, 6.1, 6078, NVIDIA GeForce GTX 1060 6GB").unwrap();
134 assert_eq!(g.index, 1);
135 assert_eq!(g.name, "NVIDIA GeForce GTX 1060 6GB");
136 assert_eq!(g.arch, GpuArch::Sm { major: 6, minor: 1 });
137 assert_eq!(g.vendor, GpuVendor::Nvidia);
138 assert_eq!(g.total_memory_mb, 6078);
139 }
140
141 #[test]
142 fn keeps_a_comma_inside_the_name() {
143 let g = parse_csv_row("0, 8.0, 81920, NVIDIA A100, 80GB").unwrap();
146 assert_eq!(g.name, "NVIDIA A100, 80GB");
147 assert_eq!(g.total_memory_mb, 81920);
148 }
149
150 #[test]
151 fn rejects_malformed_rows() {
152 assert!(parse_csv_row("0, 8.9, three").is_none());
153 assert!(parse_csv_row("x, 8.9, 24564, name").is_none());
154 assert!(parse_csv_row("0, notacap, 24564, name").is_none());
155 }
156}