1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
11pub struct GpuInfo {
12 pub index: usize,
13 pub name: String,
14 pub utilization_percent: Option<f64>,
15 pub memory_used_mb: Option<f64>,
16 pub memory_total_mb: Option<f64>,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub process_used_mb: Option<f64>,
20 pub source: String,
22}
23
24pub fn collect_gpu(pid: i32) -> Vec<GpuInfo> {
34 let mut gpus = try_nvidia_smi(pid);
36 if !gpus.is_empty() {
37 return gpus;
38 }
39
40 gpus = try_amd_sysfs();
42 gpus
43}
44
45fn query_nvidia_compute_apps(pid: i32) -> std::collections::HashMap<String, f64> {
49 let output = match std::process::Command::new("nvidia-smi")
50 .args([
51 "--query-compute-apps=pid,gpu_uuid,used_memory",
52 "--format=csv,noheader,nounits",
53 ])
54 .output()
55 {
56 Ok(o) if o.status.success() => o,
57 _ => return std::collections::HashMap::new(),
58 };
59 let stdout = String::from_utf8_lossy(&output.stdout);
60 let pid_str = pid.to_string();
61 let mut map = std::collections::HashMap::new();
62 for line in stdout.lines() {
63 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
64 if parts.len() < 3 {
65 continue;
66 }
67 if parts[0] != pid_str {
68 continue;
69 }
70 let uuid = parts[1].to_string();
71 if let Ok(used) = parts[2].parse::<f64>() {
72 *map.entry(uuid).or_insert(0.0) += used;
73 }
74 }
75 map
76}
77
78fn try_nvidia_smi(pid: i32) -> Vec<GpuInfo> {
79 let output = match std::process::Command::new("nvidia-smi")
81 .args([
82 "--query-gpu=index,uuid,name,utilization.gpu,memory.used,memory.total",
83 "--format=csv,noheader,nounits",
84 ])
85 .output()
86 {
87 Ok(o) if o.status.success() => o,
88 _ => return Vec::new(),
89 };
90
91 let process_mem = query_nvidia_compute_apps(pid);
92
93 let stdout = String::from_utf8_lossy(&output.stdout);
94 let mut gpus = Vec::new();
95
96 for line in stdout.lines() {
97 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
98 if parts.len() < 6 {
100 continue;
101 }
102 let index: usize = parts[0].parse().unwrap_or(0);
103 let uuid = parts[1].to_string();
104 let name = parts[2].to_string();
105 let util: Option<f64> = parts[3].parse().ok();
106 let mem_used: Option<f64> = parts[4].parse().ok();
107 let mem_total: Option<f64> = parts[5].parse().ok();
108 let process_used_mb = process_mem.get(&uuid).copied();
109
110 gpus.push(GpuInfo {
111 index,
112 name,
113 utilization_percent: util,
114 memory_used_mb: mem_used,
115 memory_total_mb: mem_total,
116 process_used_mb,
117 source: "nvidia-smi".to_string(),
118 });
119 }
120
121 gpus
122}
123
124fn try_amd_sysfs() -> Vec<GpuInfo> {
127 let mut gpus = Vec::new();
128
129 let drm = match std::fs::read_dir("/sys/class/drm") {
130 Ok(d) => d,
131 Err(_) => return Vec::new(),
132 };
133
134 let mut index = 0usize;
135 let mut entries: Vec<_> = drm.flatten().collect();
136 entries.sort_by_key(|e| e.file_name());
137
138 for entry in entries {
139 let name = entry.file_name();
140 let s = name.to_string_lossy();
141 if !s.starts_with("card") || s.contains('-') {
143 continue;
144 }
145
146 let base = entry.path().join("device");
147 if !base.exists() {
148 continue;
149 }
150
151 let util: Option<f64> = std::fs::read_to_string(base.join("gpu_busy_percent"))
153 .ok()
154 .and_then(|s| s.trim().parse().ok());
155
156 if util.is_none() {
157 continue;
159 }
160
161 let vram_used_mb: Option<f64> = std::fs::read_to_string(base.join("mem_info_vram_used"))
163 .ok()
164 .and_then(|s| s.trim().parse::<u64>().ok())
165 .map(|b| b as f64 / 1_048_576.0);
166
167 let vram_total_mb: Option<f64> = std::fs::read_to_string(base.join("mem_info_vram_total"))
168 .ok()
169 .and_then(|s| s.trim().parse::<u64>().ok())
170 .map(|b| b as f64 / 1_048_576.0);
171
172 let gpu_name = std::fs::read_to_string(base.join("uevent"))
174 .ok()
175 .and_then(|s| {
176 s.lines()
177 .find(|l| l.starts_with("DRIVER="))
178 .map(|l| l.to_string())
179 })
180 .unwrap_or_else(|| format!("AMD GPU ({})", s));
181
182 gpus.push(GpuInfo {
183 index,
184 name: gpu_name,
185 utilization_percent: util,
186 memory_used_mb: vram_used_mb,
187 memory_total_mb: vram_total_mb,
188 process_used_mb: None,
189 source: "sysfs/amdgpu".to_string(),
190 });
191 index += 1;
192 }
193
194 gpus
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn collect_gpu_returns_vec() {
203 let result = collect_gpu(1);
205 for g in &result {
207 assert!(!g.name.is_empty());
208 assert!(!g.source.is_empty());
209 }
210 }
211}