use rlx_runtime::{Device, device_label, parse_device};
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeviceInfo {
pub device: String,
pub name: String,
pub mem_bytes: u64,
pub unified: bool,
}
impl DeviceInfo {
fn new(device: Device, name: String, mem_bytes: u64, unified: bool) -> Self {
Self {
device: device_label(device).to_string(),
name,
mem_bytes,
unified,
}
}
pub fn kind(&self) -> Device {
parse_device(&self.device).unwrap_or(Device::Cpu)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeCaps {
pub addr: String,
pub os: String,
pub cores: usize,
pub ram_total: u64,
pub ram_avail: u64,
pub disk_free: u64,
pub devices: Vec<DeviceInfo>,
pub gflops: f64,
pub io_mbps: f64,
}
impl NodeCaps {
pub fn accel_mem(&self) -> u64 {
self.devices
.iter()
.filter(|d| d.kind() != Device::Cpu && d.mem_bytes > 0)
.map(|d| d.mem_bytes)
.max()
.unwrap_or(self.ram_total)
}
pub fn summary(&self) -> String {
let devs: Vec<String> = self
.devices
.iter()
.map(|d| device_label(d.kind()).to_string())
.collect();
format!(
"{} | {} cores | {:.0}/{:.0} GB RAM | {:.0} GFLOP/s | {}",
self.os,
self.cores,
self.ram_avail as f64 / 1e9,
self.ram_total as f64 / 1e9,
self.gflops,
devs.join("+"),
)
}
}
fn run(cmd: &str, args: &[&str]) -> Option<String> {
std::process::Command::new(cmd)
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
}
fn bench_gflops() -> f64 {
let n = 384usize;
let a = vec![1.0000117f32; n * n];
let b = vec![0.9999931f32; n * n];
let mut c = vec![0f32; n * n];
let t = Instant::now();
for i in 0..n {
for k in 0..n {
let av = a[i * n + k];
let brow = &b[k * n..k * n + n];
let crow = &mut c[i * n..i * n + n];
for j in 0..n {
crow[j] += av * brow[j];
}
}
}
std::hint::black_box(&c);
let secs = t.elapsed().as_secs_f64();
(2.0 * (n * n * n) as f64) / secs / 1e9
}
fn detect_devices(os: &str) -> Vec<DeviceInfo> {
let mut out = vec![DeviceInfo::new(Device::Cpu, cpu_name(os), 0, false)];
if os == "macos" {
let name = cpu_name(os);
out.push(DeviceInfo::new(Device::Metal, name.clone(), 0, true));
out.push(DeviceInfo::new(
Device::Ane,
format!("{name} Neural Engine"),
0,
true,
));
} else {
if let Some(s) = run(
"nvidia-smi",
&[
"--query-gpu=name,memory.total",
"--format=csv,noheader,nounits",
],
) {
for line in s.lines() {
let mut it = line.split(',');
let name = it.next().unwrap_or("CUDA GPU").trim().to_string();
let mem_mib: u64 = it.next().and_then(|m| m.trim().parse().ok()).unwrap_or(0);
out.push(DeviceInfo::new(
Device::Cuda,
name,
mem_mib * 1024 * 1024,
false,
));
}
}
let amd_gtt = amdgpu_gtt_total();
if run("rocminfo", &[]).is_some() {
out.push(DeviceInfo::new(
Device::Rocm,
"AMD ROCm".into(),
amd_gtt,
true,
));
} else if run("vulkaninfo", &["--summary"])
.map(|s| s.contains("GPU"))
.unwrap_or(false)
|| amd_gtt > 0
{
out.push(DeviceInfo::new(
Device::Vulkan,
"Vulkan GPU".into(),
amd_gtt,
true,
));
}
}
out
}
fn amdgpu_gtt_total() -> u64 {
let Ok(rd) = std::fs::read_dir("/sys/class/drm") else {
return 0;
};
for ent in rd.flatten() {
let name = ent.file_name();
let name = name.to_string_lossy();
if !name.starts_with("card") || name.contains('-') {
continue;
}
let p = ent.path().join("device/mem_info_gtt_total");
if let Ok(s) = std::fs::read_to_string(&p)
&& let Ok(v) = s.trim().parse::<u64>()
&& v > 0
{
return v;
}
}
0
}
fn cpu_name(os: &str) -> String {
if os == "macos" {
run("sysctl", &["-n", "machdep.cpu.brand_string"])
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "CPU".into())
} else {
std::fs::read_to_string("/proc/cpuinfo")
.ok()
.and_then(|s| {
s.lines()
.find(|l| l.starts_with("model name"))
.map(|l| l.split(':').nth(1).unwrap_or("CPU").trim().to_string())
})
.unwrap_or_else(|| "CPU".into())
}
}
fn ram(os: &str) -> (u64, u64) {
if os == "macos" {
let total: u64 = run("sysctl", &["-n", "hw.memsize"])
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let page: u64 = run("sysctl", &["-n", "hw.pagesize"])
.and_then(|s| s.trim().parse().ok())
.unwrap_or(16384);
let vm = run("vm_stat", &[]).unwrap_or_default();
let grab = |key: &str| -> u64 {
vm.lines()
.find(|l| l.contains(key))
.and_then(|l| l.rsplit(' ').next())
.map(|n| n.trim_end_matches('.').parse().unwrap_or(0))
.unwrap_or(0)
};
let avail =
(grab("Pages free:") + grab("Pages inactive:") + grab("Pages purgeable:")) * page;
(total, avail.min(total))
} else {
let mi = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
let kb = |key: &str| -> u64 {
mi.lines()
.find(|l| l.starts_with(key))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|n| n.parse::<u64>().ok())
.unwrap_or(0)
* 1024
};
(kb("MemTotal:"), kb("MemAvailable:"))
}
}
fn disk_free(dir: &str) -> u64 {
run("df", &["-k", dir])
.and_then(|s| s.lines().nth(1).map(String::from))
.and_then(|row| {
row.split_whitespace()
.nth(3)
.and_then(|k| k.parse::<u64>().ok())
})
.map(|k| k * 1024)
.unwrap_or(0)
}
pub fn probe_local(addr: &str, ckpt_dir: &str, bench: bool) -> NodeCaps {
let os = std::env::consts::OS.to_string();
let (ram_total, ram_avail) = ram(&os);
NodeCaps {
addr: addr.to_string(),
cores: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
ram_total,
ram_avail,
disk_free: disk_free(ckpt_dir),
devices: detect_devices(&os),
gflops: if bench { bench_gflops() } else { 0.0 },
io_mbps: 0.0,
os,
}
}
pub fn probe_remote(
ssh_host: &str,
remote_bin: &str,
addr: &str,
ckpt_dir: &str,
) -> anyhow::Result<NodeCaps> {
let cmd = format!("{remote_bin} --probe --addr {addr} --ckpt {ckpt_dir}");
let out = std::process::Command::new("ssh")
.arg(ssh_host)
.arg(&cmd)
.output()?;
if !out.status.success() {
anyhow::bail!(
"probe {ssh_host} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
let text = String::from_utf8_lossy(&out.stdout);
let json = text
.lines()
.find(|l| l.trim_start().starts_with('{'))
.unwrap_or("");
serde_json::from_str(json)
.map_err(|e| anyhow::anyhow!("parse caps from {ssh_host}: {e}: {json}"))
}