1use rlx_runtime::{Device, device_label, parse_device};
9use serde::{Deserialize, Serialize};
10use std::time::Instant;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14pub struct DeviceInfo {
15 pub device: String,
18 pub name: String,
20 pub mem_bytes: u64,
22 pub unified: bool,
25}
26
27impl DeviceInfo {
28 fn new(device: Device, name: String, mem_bytes: u64, unified: bool) -> Self {
29 Self {
30 device: device_label(device).to_string(),
31 name,
32 mem_bytes,
33 unified,
34 }
35 }
36 pub fn kind(&self) -> Device {
38 parse_device(&self.device).unwrap_or(Device::Cpu)
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct NodeCaps {
45 pub addr: String,
47 pub os: String,
49 pub cores: usize,
51 pub ram_total: u64,
53 pub ram_avail: u64,
54 pub disk_free: u64,
56 pub devices: Vec<DeviceInfo>,
58 pub gflops: f64,
60 pub io_mbps: f64,
62}
63
64impl NodeCaps {
65 pub fn accel_mem(&self) -> u64 {
72 self.devices
73 .iter()
74 .filter(|d| d.kind() != Device::Cpu && d.mem_bytes > 0)
75 .map(|d| d.mem_bytes)
76 .max()
77 .unwrap_or(self.ram_total)
78 }
79
80 pub fn summary(&self) -> String {
82 let devs: Vec<String> = self
83 .devices
84 .iter()
85 .map(|d| device_label(d.kind()).to_string())
86 .collect();
87 format!(
88 "{} | {} cores | {:.0}/{:.0} GB RAM | {:.0} GFLOP/s | {}",
89 self.os,
90 self.cores,
91 self.ram_avail as f64 / 1e9,
92 self.ram_total as f64 / 1e9,
93 self.gflops,
94 devs.join("+"),
95 )
96 }
97}
98
99fn run(cmd: &str, args: &[&str]) -> Option<String> {
100 std::process::Command::new(cmd)
101 .args(args)
102 .output()
103 .ok()
104 .filter(|o| o.status.success())
105 .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
106}
107
108fn bench_gflops() -> f64 {
110 let n = 384usize;
111 let a = vec![1.0000117f32; n * n];
112 let b = vec![0.9999931f32; n * n];
113 let mut c = vec![0f32; n * n];
114 let t = Instant::now();
115 for i in 0..n {
116 for k in 0..n {
117 let av = a[i * n + k];
118 let brow = &b[k * n..k * n + n];
119 let crow = &mut c[i * n..i * n + n];
120 for j in 0..n {
121 crow[j] += av * brow[j];
122 }
123 }
124 }
125 std::hint::black_box(&c);
126 let secs = t.elapsed().as_secs_f64();
127 (2.0 * (n * n * n) as f64) / secs / 1e9
128}
129
130fn detect_devices(os: &str) -> Vec<DeviceInfo> {
131 let mut out = vec![DeviceInfo::new(Device::Cpu, cpu_name(os), 0, false)];
132 if os == "macos" {
133 let name = cpu_name(os);
135 out.push(DeviceInfo::new(Device::Metal, name.clone(), 0, true));
136 out.push(DeviceInfo::new(
137 Device::Ane,
138 format!("{name} Neural Engine"),
139 0,
140 true,
141 ));
142 } else {
143 if let Some(s) = run(
145 "nvidia-smi",
146 &[
147 "--query-gpu=name,memory.total",
148 "--format=csv,noheader,nounits",
149 ],
150 ) {
151 for line in s.lines() {
152 let mut it = line.split(',');
153 let name = it.next().unwrap_or("CUDA GPU").trim().to_string();
154 let mem_mib: u64 = it.next().and_then(|m| m.trim().parse().ok()).unwrap_or(0);
155 out.push(DeviceInfo::new(
156 Device::Cuda,
157 name,
158 mem_mib * 1024 * 1024,
159 false,
160 ));
161 }
162 }
163 let amd_gtt = amdgpu_gtt_total();
168 if run("rocminfo", &[]).is_some() {
169 out.push(DeviceInfo::new(
170 Device::Rocm,
171 "AMD ROCm".into(),
172 amd_gtt,
173 true,
174 ));
175 } else if run("vulkaninfo", &["--summary"])
176 .map(|s| s.contains("GPU"))
177 .unwrap_or(false)
178 || amd_gtt > 0
179 {
180 out.push(DeviceInfo::new(
181 Device::Vulkan,
182 "Vulkan GPU".into(),
183 amd_gtt,
184 true,
185 ));
186 }
187 }
188 out
189}
190
191fn amdgpu_gtt_total() -> u64 {
195 let Ok(rd) = std::fs::read_dir("/sys/class/drm") else {
196 return 0;
197 };
198 for ent in rd.flatten() {
199 let name = ent.file_name();
200 let name = name.to_string_lossy();
201 if !name.starts_with("card") || name.contains('-') {
203 continue;
204 }
205 let p = ent.path().join("device/mem_info_gtt_total");
206 if let Ok(s) = std::fs::read_to_string(&p)
207 && let Ok(v) = s.trim().parse::<u64>()
208 && v > 0
209 {
210 return v;
211 }
212 }
213 0
214}
215
216fn cpu_name(os: &str) -> String {
217 if os == "macos" {
218 run("sysctl", &["-n", "machdep.cpu.brand_string"])
219 .map(|s| s.trim().to_string())
220 .unwrap_or_else(|| "CPU".into())
221 } else {
222 std::fs::read_to_string("/proc/cpuinfo")
223 .ok()
224 .and_then(|s| {
225 s.lines()
226 .find(|l| l.starts_with("model name"))
227 .map(|l| l.split(':').nth(1).unwrap_or("CPU").trim().to_string())
228 })
229 .unwrap_or_else(|| "CPU".into())
230 }
231}
232
233fn ram(os: &str) -> (u64, u64) {
234 if os == "macos" {
235 let total: u64 = run("sysctl", &["-n", "hw.memsize"])
236 .and_then(|s| s.trim().parse().ok())
237 .unwrap_or(0);
238 let page: u64 = run("sysctl", &["-n", "hw.pagesize"])
240 .and_then(|s| s.trim().parse().ok())
241 .unwrap_or(16384);
242 let vm = run("vm_stat", &[]).unwrap_or_default();
243 let grab = |key: &str| -> u64 {
244 vm.lines()
245 .find(|l| l.contains(key))
246 .and_then(|l| l.rsplit(' ').next())
247 .map(|n| n.trim_end_matches('.').parse().unwrap_or(0))
248 .unwrap_or(0)
249 };
250 let avail =
251 (grab("Pages free:") + grab("Pages inactive:") + grab("Pages purgeable:")) * page;
252 (total, avail.min(total))
253 } else {
254 let mi = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
255 let kb = |key: &str| -> u64 {
256 mi.lines()
257 .find(|l| l.starts_with(key))
258 .and_then(|l| l.split_whitespace().nth(1))
259 .and_then(|n| n.parse::<u64>().ok())
260 .unwrap_or(0)
261 * 1024
262 };
263 (kb("MemTotal:"), kb("MemAvailable:"))
264 }
265}
266
267fn disk_free(dir: &str) -> u64 {
268 run("df", &["-k", dir])
270 .and_then(|s| s.lines().nth(1).map(String::from))
271 .and_then(|row| {
272 row.split_whitespace()
273 .nth(3)
274 .and_then(|k| k.parse::<u64>().ok())
275 })
276 .map(|k| k * 1024)
277 .unwrap_or(0)
278}
279
280pub fn probe_local(addr: &str, ckpt_dir: &str, bench: bool) -> NodeCaps {
283 let os = std::env::consts::OS.to_string();
284 let (ram_total, ram_avail) = ram(&os);
285 NodeCaps {
286 addr: addr.to_string(),
287 cores: std::thread::available_parallelism()
288 .map(|n| n.get())
289 .unwrap_or(1),
290 ram_total,
291 ram_avail,
292 disk_free: disk_free(ckpt_dir),
293 devices: detect_devices(&os),
294 gflops: if bench { bench_gflops() } else { 0.0 },
295 io_mbps: 0.0,
296 os,
297 }
298}
299
300pub fn probe_remote(
303 ssh_host: &str,
304 remote_bin: &str,
305 addr: &str,
306 ckpt_dir: &str,
307) -> anyhow::Result<NodeCaps> {
308 let cmd = format!("{remote_bin} --probe --addr {addr} --ckpt {ckpt_dir}");
309 let out = std::process::Command::new("ssh")
310 .arg(ssh_host)
311 .arg(&cmd)
312 .output()?;
313 if !out.status.success() {
314 anyhow::bail!(
315 "probe {ssh_host} failed: {}",
316 String::from_utf8_lossy(&out.stderr)
317 );
318 }
319 let text = String::from_utf8_lossy(&out.stdout);
320 let json = text
321 .lines()
322 .find(|l| l.trim_start().starts_with('{'))
323 .unwrap_or("");
324 serde_json::from_str(json)
325 .map_err(|e| anyhow::anyhow!("parse caps from {ssh_host}: {e}: {json}"))
326}