use ferrotherm::{gibbs::Sampler, ising::lattice2d, wgsl::GpuModel};
use std::time::Instant;
fn fit(points: &[(f64, f64)]) -> (f64, f64) {
let n = points.len() as f64;
let sx: f64 = points.iter().map(|p| p.0).sum();
let sy: f64 = points.iter().map(|p| p.1).sum();
let sxx: f64 = points.iter().map(|p| p.0 * p.0).sum();
let sxy: f64 = points.iter().map(|p| p.0 * p.1).sum();
let d = n * sxx - sx * sx;
if d.abs() < 1e-12 { return (sy / n, 0.0); }
let b = (n * sxy - sx * sy) / d;
((sy - b * sx) / n, b)
}
fn worst_residual(points: &[(f64, f64)], a: f64, b: f64) -> f64 {
points.iter()
.map(|&(k, ms)| ((a + b * k) - ms).abs() / ms.max(1e-9))
.fold(0.0f64, f64::max)
}
fn median(mut v: Vec<f64>) -> f64 {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[v.len() / 2]
}
fn minimum(v: &[f64]) -> f64 { v.iter().cloned().fold(f64::INFINITY, f64::min) }
fn load_average() -> Option<f64> {
let out = std::process::Command::new("uptime").output().ok()?;
let s = String::from_utf8_lossy(&out.stdout);
let tail = s.rsplit("load averages:").next().or_else(|| s.rsplit("load average:").next())?;
tail.split_whitespace().next()?.trim_end_matches(',').parse().ok()
}
fn main() {
let Some(gpu) = ferrotherm_gpu::Gpu::new() else {
eprintln!("no GPU adapter on this machine; nothing to measure");
return;
};
let a = gpu.adapter();
println!("adapter: {} ({:?}, {:?})", a.name, a.device_type, a.backend);
if !gpu.is_hardware() {
println!();
println!("This is a software rasteriser. Every figure below describes a CPU pretending to");
println!("be a GPU, so they are printed and NOT reported as a fabric measurement.");
}
println!();
println!("PREDICTED before measuring: the intercept is milliseconds, dominates at small k, and");
println!("implies a resident clamp rate at least 10x the rate a single call achieves.");
println!();
let ks = [1usize, 2, 4, 8, 16, 32, 64, 128, 256];
let repeats = 21;
let beta = 0.7;
match load_average() {
Some(la) if la > 1.5 => {
println!("load average {la:.2}: this machine is running other work. The minimum column");
println!("is the figure to read; the median column is what it managed while sharing.");
println!();
}
Some(la) => { println!("load average {la:.2}"); println!(); }
None => {}
}
println!("=== cold start: the first call on this process ===");
println!("{:>6} {:>8} {:>12}", "l", "nodes", "cold ms");
let mut cold = Vec::new();
for l in [16usize, 64, 256] {
let g = lattice2d(l, 1.0);
let m = GpuModel::from_graph(&g);
let mut s = vec![1i8; l * l];
let t0 = Instant::now();
gpu.sweep(&m, &mut s, beta, 1).unwrap();
let ms = t0.elapsed().as_secs_f64() * 1e3;
cold.push((l, ms));
println!("{:>6} {:>8} {:>12.3}", l, l * l, ms);
}
println!();
println!("=== restream against clamp, warm ===");
println!("Fitting ms = restream + clamp*k over k = 1..256, {repeats} runs per point.");
println!("The fit is on the minimum series. The median series is printed beside it.");
println!();
println!(
"{:>6} {:>8} {:>12} {:>12} {:>9} {:>11} {:>13} {:>7} {:>9} {:>9}",
"l", "nodes", "restream ms", "clamp ms", "ratio", "call Hz", "resident Hz", "fit err", "med/min", "pass/pass"
);
let mut rows = Vec::new();
for l in [16usize, 32, 64, 128, 256, 512] {
let g = lattice2d(l, 1.0);
let n = l * l;
let m = GpuModel::from_graph(&g);
let mut s = vec![1i8; n];
gpu.sweep(&m, &mut s, beta, 1).unwrap();
let mut passes: Vec<(f64, f64, f64, f64)> = Vec::new();
for pass in 0..2 {
let mut lo = Vec::new();
let mut mid = Vec::new();
for &k in &ks {
let runs: Vec<f64> = (0..repeats)
.map(|_| {
let t0 = Instant::now();
gpu.sweep(&m, &mut s, beta, k as u32).unwrap();
t0.elapsed().as_secs_f64() * 1e3
})
.collect();
lo.push((k as f64, minimum(&runs)));
mid.push((k as f64, median(runs)));
}
print!(" min l={:<4} p{}", l, pass + 1);
for &(k, ms) in &lo { print!(" {}:{:.3}", k as u32, ms); }
println!();
let (restream, clamp) = fit(&lo);
let err = worst_residual(&lo, restream, clamp);
let spread = mid.last().unwrap().1 / lo.last().unwrap().1;
passes.push((restream, clamp, err, spread));
}
let (r1, c1, e1, sp) = passes[0];
let (r2, c2, e2, _) = passes[1];
let restream = 0.5 * (r1 + r2);
let clamp = 0.5 * (c1 + c2);
let err = e1.max(e2);
let agree = (r1.max(r2) / r1.min(r2)).max(c1.max(c2) / c1.min(c2));
let call_hz = 1e3 / (restream + clamp).max(1e-9);
let resident_hz = 1e3 / clamp.max(1e-9);
let ratio = restream / clamp.max(1e-9);
rows.push((l, n, restream, clamp, call_hz, resident_hz));
println!(
"{:>6} {:>8} {:>12.3} {:>12.4} {:>8.1}x {:>11.1} {:>13.1} {:>6.1}% {:>8.2}x {:>8.2}x",
l, n, restream, clamp, ratio, call_hz, resident_hz, err * 100.0, sp, agree
);
}
println!();
println!("=== against the CPU path on the same models ===");
println!("A controller with a small model may not need the fabric at all, and a fabric figure");
println!("that never asks this question is comparing against nothing.");
println!();
println!("{:>6} {:>8} {:>14} {:>14}", "l", "nodes", "cpu ms/sweep", "gpu ms/sweep");
for &(l, n, _, clamp, _, _) in &rows {
let g = lattice2d(l, 1.0);
let cpu_ms = median(
(0..repeats)
.map(|r| {
let mut sim = Sampler::new(&g, beta, r as u64 + 1);
let t0 = Instant::now();
sim.sweeps(32, None);
t0.elapsed().as_secs_f64() * 1e3 / 32.0
})
.collect(),
);
println!("{:>6} {:>8} {:>14.4} {:>14.4}", l, n, cpu_ms, clamp);
}
println!();
println!("=== what fits under a control deadline ===");
println!("Sweeps available per tick at each rate, under the current API and with the model");
println!("resident. A count of zero means the tick is spent before a single sweep runs.");
println!();
print!("{:>6} {:>8}", "l", "nodes");
for hz in [10, 30, 50, 100] { print!(" {:>8}", format!("{hz} Hz now")); }
for hz in [10, 30, 50, 100] { print!(" {:>10}", format!("{hz} Hz res")); }
println!();
for &(l, n, restream, clamp, _, _) in &rows {
print!("{:>6} {:>8}", l, n);
for hz in [10.0, 30.0, 50.0, 100.0] {
let budget = 1e3 / hz;
let k = ((budget - restream) / clamp.max(1e-9)).floor().max(0.0);
print!(" {:>8}", k as i64);
}
for hz in [10.0, 30.0, 50.0, 100.0] {
let budget = 1e3 / hz;
let k = (budget / clamp.max(1e-9)).floor().max(0.0);
print!(" {:>10}", k as i64);
}
println!();
}
println!();
println!("The restream column is a property of this API, not of this hardware: `Gpu::sweep`");
println!("builds every buffer, the shader module and the pipeline inside the call. The clamp");
println!("column is what the fabric does. Whichever of the two is larger is the one that sets");
println!("the conditioning bandwidth, and only one of them needs new physics to move.");
}