use ferrotherm::{gibbs::Sampler, ising::lattice2d, ledger::Ledger, wgsl::GpuModel};
use std::time::Duration;
fn main() {
let Some(gpu) = ferrotherm_gpu::Gpu::new() else {
eprintln!("no GPU adapter; nothing to compare");
return;
};
let Some(mut meter) = ferrotherm_meter::Meter::detect() else {
eprintln!("no power backend; the timing comparison lives in bench.rs");
return;
};
println!("adapter : {} ({:?})", gpu.adapter().name, gpu.adapter().device_type);
println!("machine : {}", meter.machine());
if !gpu.is_hardware() {
println!("\nSoftware rasteriser. Numbers below describe a CPU twice over; not reported as a ratio.");
}
println!("\nsettling...");
std::thread::sleep(Duration::from_secs(5));
let l = 512usize;
let g = lattice2d(l, 1.0);
let n = (l * l) as u64;
let sweeps = 200u32;
let window = Duration::from_secs(4);
println!("model : {l}x{l} = {n} nodes; each path repeats {sweeps}-sweep passes for {:.0} s\n",
window.as_secs_f64());
let mut row = |label: &str, pass: &mut dyn FnMut()| -> Option<(f64, f64, f64, u64)> {
std::thread::sleep(Duration::from_secs(10));
let idle = match meter.idle(Duration::from_secs(3)) {
Ok(w) => w,
Err(e) => { eprintln!("{label}: {e}"); return None; }
};
let mut passes = 0u64;
let m = match meter.measure(idle, || {
let t0 = std::time::Instant::now();
while t0.elapsed() < window {
pass();
passes += 1;
}
}) {
Ok(m) => m,
Err(e) => { eprintln!("{label}: {e}"); return None; }
};
let updates = passes * n * sweeps as u64;
let per = m.joules_above_idle / updates as f64;
println!(
" {label:<4}: {passes} passes ({updates} updates) in {:.2} s at {:.1} W over {:.1} W \
idle = {:.1} J -> {per:.3e} J/update",
m.seconds, m.mean_watts, m.idle_watts, m.joules_above_idle
);
Some((m.seconds, m.joules_above_idle, per, updates))
};
let gm = GpuModel::from_graph(&g);
let mut spins = vec![1i8; n as usize];
gpu.sweep(&gm, &mut spins, 0.7, 1).unwrap(); let g_res = row("gpu", &mut || { gpu.sweep(&gm, &mut spins, 0.7, sweeps).unwrap(); });
let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
println!(" (cpu uses all {threads} cores; comparing a whole GPU to one core is not a comparison)\n");
let mut led = Ledger::default();
let c_res = row("cpu", &mut || {
let mut s = Sampler::new(&g, 0.7, 1);
s.sweeps_par(sweeps as usize, threads, Some(&mut led));
});
let (Some((_, gj, gp, gu)), Some((_, cj, cp, cu))) = (g_res, c_res) else {
eprintln!("\nboth sides have to be measurable for the comparison to mean anything");
return;
};
assert_eq!(led.samples, cu, "the ledger and the pass count must agree on the CPU's work");
println!("\n throughput : gpu {:.3e} vs cpu {:.3e} updates/s -> {:.1}x faster",
gu as f64 / window.as_secs_f64(), cu as f64 / window.as_secs_f64(),
gu as f64 / cu as f64);
println!(" energy : gpu {gj:.1} J for {gu} updates, cpu {cj:.1} J for {cu} -> {:.1}x cheaper per update",
cp / gp);
println!(" per op : gpu {gp:.3e} vs cpu {cp:.3e} J/update");
println!();
if cp / gp < gu as f64 / cu as f64 {
println!(" The speedup is larger than the saving, so the GPU is drawing more power while it");
println!(" works -- which is the expected shape. Time and joules are different questions and");
println!(" a stack that prices in joules has to answer the second one on its own terms.");
}
println!(" Both sides: whole-system wall power above idle, divided by counted node updates, on");
println!(" one machine. Neither is a datasheet.");
}