use rlx_runtime::{Device, device_label, is_available, parse_device};
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeviceInfo {
pub device: String,
pub name: String,
pub mem_bytes: u64,
pub unified: bool,
#[serde(default)]
pub gflops: f64,
#[serde(default = "default_true")]
pub available: bool,
}
fn default_true() -> bool {
true
}
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,
gflops: 0.0,
available: is_available(device),
}
}
pub fn parsed(&self) -> Option<Device> {
parse_device(&self.device).ok()
}
pub fn kind(&self) -> Device {
self.parsed().unwrap_or(Device::Cpu)
}
pub fn is_recognized(&self) -> bool {
self.parsed().is_some()
}
pub fn is_usable(&self) -> bool {
self.is_recognized() && self.available
}
}
#[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.is_usable() && d.kind() != Device::Cpu && d.mem_bytes > 0)
.map(|d| d.mem_bytes)
.max()
.unwrap_or(self.ram_total)
}
pub fn gflops_for(&self, dev: Device) -> f64 {
self.devices
.iter()
.find(|d| d.parsed() == Some(dev) && d.gflops > 0.0)
.map(|d| d.gflops)
.unwrap_or(self.gflops)
}
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 bench_device_gflops(dev: Device) -> Option<f64> {
use rlx_ir::{DType, Graph, Shape};
if !is_available(dev) {
return None;
}
let n = 4096usize;
let rows = 1usize;
const ROUND_SECS: f64 = 0.03;
const ROUNDS: usize = 3;
let run_once = || -> Option<f64> {
let mut g = Graph::new("caps_bench");
let a = g.input("a", Shape::new(&[rows, n], DType::F32));
let b = g.param("b", Shape::new(&[n, n], DType::F32));
let c = g.matmul(a, b, Shape::new(&[rows, n], DType::F32));
g.set_outputs(vec![c]);
let mut compiled = rlx_runtime::Session::new(dev).compile(g);
compiled.set_param("b", &vec![0.5f32; n * n]);
let x = vec![0.25f32; rows * n];
let t = Instant::now();
let _ = compiled.run(&[("a", x.as_slice())]);
let first = t.elapsed().as_secs_f64().max(1e-6);
let iters = ((ROUND_SECS / first).ceil() as usize).clamp(2, 4096);
let flop = 2.0 * (rows * n * n) as f64 * iters as f64;
let mut best = 0.0f64;
for _ in 0..ROUNDS {
let t = Instant::now();
for _ in 0..iters {
let _ = compiled.run(&[("a", x.as_slice())]);
}
let secs = t.elapsed().as_secs_f64();
if secs > 0.0 {
best = best.max(flop / secs / 1e9);
}
}
(best > 0.0).then_some(best)
};
static HOOK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = HOOK.lock().unwrap_or_else(|e| e.into_inner());
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run_once))
.ok()
.flatten();
std::panic::set_hook(hook);
out
}
#[cfg(unix)]
fn no_cache_hint(f: &std::fs::File, off: u64, len: u64) {
use std::os::fd::AsRawFd;
let fd = f.as_raw_fd();
unsafe {
#[cfg(target_vendor = "apple")]
{
let _ = (off, len);
libc::fcntl(fd, libc::F_NOCACHE, 1);
}
#[cfg(target_os = "linux")]
{
libc::posix_fadvise(fd, off as i64, len as i64, libc::POSIX_FADV_DONTNEED);
}
#[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
{
let _ = (fd, off, len);
}
}
}
#[cfg(not(unix))]
fn no_cache_hint(_f: &std::fs::File, _off: u64, _len: u64) {}
#[cfg(unix)]
fn resident_fraction(f: &std::fs::File, off: u64, len: usize) -> Option<f64> {
use std::os::fd::AsRawFd;
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page <= 0 {
return None;
}
let page = page as u64;
let base = off - (off % page);
let span = (len as u64 + (off - base)) as usize;
let pages = span.div_ceil(page as usize);
unsafe {
let addr = libc::mmap(
std::ptr::null_mut(),
span,
libc::PROT_READ,
libc::MAP_SHARED,
f.as_raw_fd(),
base as libc::off_t,
);
if addr == libc::MAP_FAILED {
return None;
}
let mut vec = vec![0u8; pages];
let rc = libc::mincore(addr, span, vec.as_mut_ptr().cast());
libc::munmap(addr, span);
if rc != 0 {
return None;
}
let hot = vec.iter().filter(|b| *b & 1 != 0).count();
Some(hot as f64 / pages.max(1) as f64)
}
}
#[cfg(not(unix))]
fn resident_fraction(_f: &std::fs::File, _off: u64, _len: usize) -> Option<f64> {
None
}
fn bench_io_mbps(dir: &str) -> f64 {
const CHUNK: usize = 64 << 20;
const TRIES: usize = 8;
const COLD: f64 = 0.10;
if let Some((path, len)) = largest_file(dir)
&& len > (8 << 20)
&& let Ok(mut f) = std::fs::File::open(&path)
{
use std::io::{Read, Seek, SeekFrom};
let want = CHUNK.min(len as usize);
let span = len as usize - want;
let mut chosen = None;
for i in 0..TRIES {
let off = if span > 0 {
(nanos() as usize).wrapping_mul(i * 2 + 1) % span
} else {
0
};
match resident_fraction(&f, off as u64, want) {
None => {
chosen = Some(off);
break;
}
Some(r) if r <= COLD => {
chosen = Some(off);
break;
}
Some(_) => {
if span == 0 {
break; }
}
}
}
if let Some(off) = chosen {
no_cache_hint(&f, off as u64, want as u64);
if f.seek(SeekFrom::Start(off as u64)).is_ok() {
let mut buf = vec![0u8; want];
let t = Instant::now();
if f.read_exact(&mut buf).is_ok() {
let secs = t.elapsed().as_secs_f64();
std::hint::black_box(&buf);
if secs > 0.0 {
return want as f64 / 1e6 / secs;
}
}
}
}
}
write_probe_mbps(dir)
}
fn nanos() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(1)
}
fn largest_file(dir: &str) -> Option<(std::path::PathBuf, u64)> {
let rd = std::fs::read_dir(dir).ok()?;
rd.flatten()
.filter_map(|e| {
let md = e.metadata().ok()?;
md.is_file().then(|| (e.path(), md.len()))
})
.max_by_key(|(_, l)| *l)
}
fn write_probe_mbps(dir: &str) -> f64 {
use std::io::Write;
const N: usize = 16 << 20;
let path = std::path::Path::new(dir).join(format!(".rlx-io-probe-{}", nanos()));
let buf = vec![0xA5u8; N];
let t = Instant::now();
let ok = (|| -> std::io::Result<()> {
let mut f = std::fs::File::create(&path)?;
f.write_all(&buf)?;
f.sync_all()
})()
.is_ok();
let secs = t.elapsed().as_secs_f64();
let _ = std::fs::remove_file(&path);
if ok && secs > 0.0 {
N as f64 / 1e6 / secs
} else {
0.0
}
}
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);
let mut devices = detect_devices(&os);
if bench {
for d in devices.iter_mut() {
d.gflops = bench_device_gflops(d.kind()).unwrap_or(0.0);
}
}
let gflops = if bench {
devices
.iter()
.map(|d| d.gflops)
.fold(0.0f64, f64::max)
.max(0.0)
} else {
0.0
};
let gflops = if bench && gflops <= 0.0 {
bench_gflops()
} else {
gflops
};
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,
gflops,
io_mbps: if bench { bench_io_mbps(ckpt_dir) } else { 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}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(name: &str, bytes: usize) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("rlx-caps-{name}"));
let _ = std::fs::create_dir_all(&dir);
let p = dir.join("probe.bin");
std::fs::write(&p, vec![7u8; bytes]).unwrap();
p
}
#[cfg(unix)]
#[test]
fn residency_is_observable_and_tracks_reads() {
let p = scratch("residency", 4 << 20);
let f = std::fs::File::open(&p).unwrap();
let hot = resident_fraction(&f, 0, 4 << 20)
.expect("mincore must answer on a plain file-backed mapping");
assert!(
hot > 0.5,
"a file this process just wrote reads as {hot:.2} resident — the \
residency probe is not seeing the page cache, so the disk bench \
cannot tell RAM from storage"
);
let _ = std::fs::remove_dir_all(p.parent().unwrap());
}
#[test]
fn a_fully_cached_checkpoint_does_not_report_ram_bandwidth() {
let p = scratch("cached", 32 << 20);
let dir = p.parent().unwrap().to_str().unwrap().to_string();
let _ = std::fs::read(&p).unwrap();
let mbps = bench_io_mbps(&dir);
assert!(mbps > 0.0 && mbps.is_finite(), "{mbps}");
assert!(
mbps < 10_000.0,
"{mbps:.0} MB/s is memory bandwidth, not a disk — the planner would \
price expert paging at roughly nothing"
);
let _ = std::fs::remove_dir_all(p.parent().unwrap());
}
}