open-standards/wai/tools/ops-gpu-bench/src/main.rs
Transaction Science 6072734a23 wai-ops-gpu, ops-gpu-bench: clippy's notes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 04:47:31 -04:00

529 lines
25 KiB
Rust

//! `ops-gpu-bench`: time, and energy where this host has a counter for it, of one decode
//! of each committed vector of each capability that runs a `wai-ops/1` graph, with its
//! graphs on the CPU (the reference sink's default: the fast kernels on every core) and
//! on each GPU adapter found (`wai::int_ops::gpu`).
//!
//! ops-gpu-bench [--secs S] [--min-runs N] [--only <capability>]
//!
//! with `WAI_JPEGAI_CODESTREAM`, `WAI_JPEGAI_WEIGHTS_DIR` and `WAI_JPEGAI_NORM` naming the
//! `jpegai/v1` inputs to include `wai.image.jpegai`'s exact decoder.
//!
//! **Method.** For each vector and each path: two decodes to warm up (on a GPU these
//! compile nothing, but upload the graph's parameters once), then decodes back to back
//! until at least `S` seconds (default 3) and `N` decodes (default 5) have run. Each
//! decode's wall time is recorded; the line gives the count, the median and the 10th and
//! 90th percentiles. Every decode's output is compared to the CPU's first, byte for byte.
//! The energy window is the timed loop, one window per line, read only at its two ends:
//!
//! - macOS on Apple Silicon: this process's CPU energy and its share of the GPU rail
//! (`jouleclaw_energy::span::ProcessSpanMeter`, IOReport), which gives no GPU figure
//! where another process used the GPU over the window;
//! - Linux: the RAPL `package-0` counter (`/sys/class/powercap/intel-rapl:0`), the whole
//! package, every process on it included; and on a host with the NVIDIA driver, the
//! driver's cumulative energy counter for GPU 0 (`nvmlDeviceGetTotalEnergyConsumption`,
//! the board), read at the two ends only, since a counter read every ~100 ms has been
//! seen to over-read on this hardware. Before the decodes, one idle window of `S`
//! seconds gives each counter's idle power, printed with the lines.
//!
//! The printed lines are the measurement; nothing here turns them into a claim.
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use serde_json::Value;
use wai::int_hyper_synth::IntModel;
use wai::int_ops::gpu::{self, wai_ops_gpu};
use wai::int_payload::{IntDecoded, IntParams, decode_int};
use wai::pin::HeldBytes;
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn read(rel: &str) -> Vec<u8> {
std::fs::read(wai_dir().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
}
fn json(rel: &str) -> Value {
serde_json::from_slice(&read(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
}
struct Case {
cap: &'static str,
name: String,
run: Box<dyn Fn() -> Vec<u8>>,
}
fn cap_static(c: &str) -> Option<&'static str> {
["wai.neural.int_hyper", "wai.video.int_hyper", "wai.audio.int_codec", "wai.splat.int_codec"].into_iter().find(|k| *k == c)
}
fn bytes_of(d: &IntDecoded) -> Vec<u8> {
match d {
IntDecoded::Image { rgb, .. } => rgb.clone(),
IntDecoded::Video { frames, .. } => frames.concat(),
IntDecoded::Pcm { samples, .. } => samples.iter().flat_map(|s| s.to_le_bytes()).collect(),
IntDecoded::Attr { bytes, .. } => bytes.clone(),
_ => panic!("an output form this tool does not know"),
}
}
fn cases() -> Vec<Case> {
let mut out = Vec::new();
// The payload vectors whose capability runs a graph (`wai.video.int_motion` runs none).
for e in json("int-payload-conformance/vectors.json")["vectors"].as_array().unwrap() {
let Some(cap) = cap_static(e["capability"].as_str().unwrap()) else { continue };
let payload = read(e["payload"].as_str().unwrap());
let set: BTreeMap<String, Vec<u8>> = e["set"].as_object().into_iter().flatten().map(|(k, v)| (k.clone(), read(v.as_str().unwrap()))).collect();
let run = move || {
let view: BTreeMap<String, &[u8]> = set.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect();
let params = wai::int_payload::resolve(cap, None, &[HeldBytes::Set { listing: b"", files: &view }], None).unwrap_or_else(|_| IntParams::none());
bytes_of(&decode_int(cap, &payload, &params).expect("a committed vector decodes"))
};
out.push(Case { cap, name: e["name"].as_str().unwrap().into(), run: Box::new(run) });
}
for name in json("neural-conformance/expected_hashes.json").as_object().unwrap().keys() {
let dir = format!("neural-conformance/vectors/{name}");
let (payload, set) = (read(&format!("{dir}/latent.bin")), read(&format!("{dir}/int_synth.json")));
out.push(Case { cap: "wai.neural.int_synth", name: name.clone(), run: Box::new(move || wai::int_synth::decode_from_spec(&payload, &set).unwrap().rgb) });
}
{
const DIR: &str = "wai-rs/tests/vectors/int_mlicpp";
let m = wai::int_mlicpp::IntMlicModel::from_bundle(&read(&format!("{DIR}/model.wmb"))).unwrap();
let p = wai::container::Wai::from_bytes(&read(&format!("{DIR}/stream.wai"))).unwrap().payload;
out.push(Case { cap: "wai.neural.int_mlicpp", name: "int_mlicpp".into(), run: Box::new(move || wai::int_mlicpp::decode(&m, &p).unwrap().rgb) });
}
for (name, dir, js, t, g) in [("world", "int_world", "world.json", "world_T.bin", "world_gs.bin"), ("world2", "int_world2", "world2.json", "world2_T.bin", "world2_gs.bin")] {
let j = json(&format!("wai-rs/tests/vectors/{dir}/{js}"));
let tm = IntModel::from_bytes(&read(&format!("wai-rs/tests/vectors/{dir}/{t}"))).unwrap();
let gm = IntModel::from_bytes(&read(&format!("wai-rs/tests/vectors/{dir}/{g}"))).unwrap();
let (c, lat) = (j["c"].as_u64().unwrap() as usize, j["lat"].as_u64().unwrap() as usize);
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect();
let afrac = j["afrac"].as_u64().unwrap() as u32;
let q = |d: i64| wai_ops::stage::dequantize(d, afrac, 0).unwrap();
let mv = |a: &Value| wai::int_world::ACTIONS[a.as_u64().unwrap() as usize];
let rows: Vec<Vec<i64>> = match j.get("pairs") {
Some(p) => p.as_array().unwrap().iter().map(|p| {
let ((xa, ya), (xb, yb)) = (mv(&p[0]), mv(&p[1]));
vec![q(xa), q(ya), q(xb), q(yb)]
}).collect(),
None => j["actions"].as_array().unwrap().iter().map(|a| {
let (dx, dy) = mv(a);
vec![q(dx), q(dy), 0]
}).collect(),
};
out.push(Case { cap: "wai.world.model", name: name.into(), run: Box::new(move || wai::int_world::decode_planes(&tm, &gm, &z0, &rows, c, lat).unwrap().frames.concat()) });
}
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty());
if let (Some(cs), Some(dir), Some(nf)) = (env("WAI_JPEGAI_CODESTREAM"), env("WAI_JPEGAI_WEIGHTS_DIR"), env("WAI_JPEGAI_NORM")) {
let (data, weights, norm) = jpegai::load(&cs, &dir, &nf);
out.push(Case { cap: "wai.image.jpegai", name: "jpegai/v1 test256".into(), run: Box::new(move || wai::jpegai_decode::decode(&data, &weights, &norm).unwrap().data) });
} else {
println!("ops-gpu bench: wai.image.jpegai not measured: WAI_JPEGAI_CODESTREAM, WAI_JPEGAI_WEIGHTS_DIR and WAI_JPEGAI_NORM are not all set");
}
out
}
mod jpegai {
use wai::jpegai_decode::{DecoderWeights, NormTables, PlaneWeights};
use wai::jpegai_icci::{Conv, IcciNet, IcciPath, ResBlock};
use wai::jpegai_synthesis::{IntNet, Weights};
fn jload(p: &std::path::Path) -> serde_json::Value {
serde_json::from_slice(&std::fs::read(p).unwrap()).unwrap()
}
fn w_of(dir: &str, name: &str) -> Weights {
Weights::from_json(&jload(&std::path::Path::new(dir).join(name))).unwrap()
}
fn int_of(dir: &str, name: &str) -> IntNet {
IntNet::from_json(&jload(&std::path::Path::new(dir).join(name))).unwrap()
}
fn i64v(v: &serde_json::Value, k: &str) -> Vec<i64> {
v[k].as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect()
}
fn i32v(v: &serde_json::Value, k: &str) -> Vec<i32> {
v[k].as_array().unwrap().iter().map(|x| x.as_i64().unwrap() as i32).collect()
}
fn f3(v: &serde_json::Value, k: &str) -> [f32; 3] {
let a = v[k].as_array().unwrap();
[a[0].as_f64().unwrap() as f32, a[1].as_f64().unwrap() as f32, a[2].as_f64().unwrap() as f32]
}
fn i3(v: &serde_json::Value, k: &str) -> [i64; 3] {
let a = v[k].as_array().unwrap();
[a[0].as_i64().unwrap(), a[1].as_i64().unwrap(), a[2].as_i64().unwrap()]
}
fn conv_of(c: &serde_json::Value) -> Conv {
let g = |k: &str| c[k].as_u64().unwrap() as usize;
Conv {
w: c["w"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap() as f32).collect(),
b: c["b"].as_array().unwrap().iter().map(|x| x.as_f64().unwrap() as f32).collect(),
cin: g("cin"),
cout: g("cout"),
kh: g("kh"),
kw: g("kw"),
ph: g("ph"),
pw: g("pw"),
}
}
fn path_of(v: &serde_json::Value) -> IcciPath {
IcciPath {
conv_first: conv_of(&v["conv_first"]),
blocks: v["blocks"].as_array().unwrap().iter().map(|b| ResBlock { conv1: conv_of(&b["c1"]), conv2: conv_of(&b["c2"]), scale: b["scale"].as_f64().unwrap() as f32 }).collect(),
conv_last: conv_of(&v["conv_last"]),
scale: v["scale"].as_f64().unwrap() as f32,
}
}
pub fn load(cs: &str, dir: &str, nf: &str) -> (Vec<u8>, DecoderWeights, NormTables) {
let data = std::fs::read(cs).unwrap();
let nv = jload(std::path::Path::new(nf));
let iw = jload(&std::path::Path::new(dir).join("icci.json"));
let icci = IcciNet { luma: path_of(&iw["luma"]), chroma: path_of(&iw["chroma"]), conv_last_v: conv_of(&iw["conv_last_v"]), scale_v: iw["scale_v"].as_f64().unwrap() as f32 };
let weights = DecoderWeights {
luma: PlaneWeights { hyper_scale: int_of(dir, "hs_y.json"), hyper_decoder: w_of(dir, "hyp_y.json"), synthesis: w_of(dir, "synth_y.json") },
chroma: PlaneWeights { hyper_scale: int_of(dir, "hs_uv.json"), hyper_decoder: w_of(dir, "hyp_uv.json"), synthesis: w_of(dir, "synth_uv.json") },
mcm_y: [w_of(dir, "mcm0_y.json"), w_of(dir, "mcm1_y.json"), w_of(dir, "mcm2_y.json"), w_of(dir, "mcm3_y.json")],
icci,
};
let norm = NormTables {
pmf: nv["pmf"].as_array().unwrap().iter().map(|r| r.as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect()).collect(),
bounds: i64v(&nv, "bounds"),
per_channel: nv["per_channel"].as_u64().unwrap() as usize,
cdfs_y: i64v(&nv, "cdfs_y"),
cdfs_uv: i64v(&nv, "cdfs_uv"),
gain_y: i64v(&nv, "gain_y"),
gain_uv: i64v(&nv, "gain_uv"),
lsbs_t0: i32v(&nv, "lsbs_t0"),
lsbs_t1: i32v(&nv, "lsbs_t1"),
lef_thr: i3(&nv, "lef_thr0"),
lef_mag: f3(&nv, "lef_mag0"),
};
(data, weights, norm)
}
}
// ---------------------------------------------------------------- energy, Linux
#[cfg(target_os = "linux")]
mod meter {
use std::time::{Duration, Instant};
/// The RAPL package-0 counter, µJ, with its wrap range.
pub struct Rapl {
range: u64,
}
const RAPL: &str = "/sys/class/powercap/intel-rapl:0";
impl Rapl {
pub fn open() -> Result<Rapl, String> {
let name = std::fs::read_to_string(format!("{RAPL}/name")).map_err(|e| format!("{RAPL}/name: {e}"))?;
if name.trim() != "package-0" {
return Err(format!("{RAPL} is {:?}, not package-0", name.trim()));
}
let range = std::fs::read_to_string(format!("{RAPL}/max_energy_range_uj")).map_err(|e| e.to_string())?.trim().parse().map_err(|e| format!("{e}"))?;
let r = Rapl { range };
r.read()?;
Ok(r)
}
pub fn read(&self) -> Result<u64, String> {
std::fs::read_to_string(format!("{RAPL}/energy_uj")).map_err(|e| format!("{RAPL}/energy_uj: {e}"))?.trim().parse().map_err(|e| format!("{e}"))
}
pub fn delta(&self, a: u64, b: u64) -> u64 {
if b >= a { b - a } else { b + self.range + 1 - a }
}
}
/// The NVIDIA driver's cumulative energy counter for GPU 0, mJ, through its library
/// opened at run time.
pub struct Nvml {
dev: *mut core::ffi::c_void,
energy: unsafe extern "C" fn(*mut core::ffi::c_void, *mut u64) -> i32,
power: unsafe extern "C" fn(*mut core::ffi::c_void, *mut u32) -> i32,
}
impl Nvml {
pub fn open() -> Result<Nvml, String> {
// SAFETY: dlopen/dlsym with NUL-terminated names; each symbol is cast to the
// signature NVML's header gives it, and is called only after a non-null check.
unsafe {
let h = libc::dlopen(c"libnvidia-ml.so.1".as_ptr(), libc::RTLD_NOW);
if h.is_null() {
return Err("libnvidia-ml.so.1 is not on this host".into());
}
let sym = |n: &core::ffi::CStr| {
let p = libc::dlsym(h, n.as_ptr());
if p.is_null() { Err(format!("no symbol {n:?}")) } else { Ok(p) }
};
let init: unsafe extern "C" fn() -> i32 = core::mem::transmute(sym(c"nvmlInit_v2")?);
let by_index: unsafe extern "C" fn(u32, *mut *mut core::ffi::c_void) -> i32 = core::mem::transmute(sym(c"nvmlDeviceGetHandleByIndex_v2")?);
let energy = core::mem::transmute::<*mut core::ffi::c_void, unsafe extern "C" fn(*mut core::ffi::c_void, *mut u64) -> i32>(sym(c"nvmlDeviceGetTotalEnergyConsumption")?);
let power = core::mem::transmute::<*mut core::ffi::c_void, unsafe extern "C" fn(*mut core::ffi::c_void, *mut u32) -> i32>(sym(c"nvmlDeviceGetPowerUsage")?);
if init() != 0 {
return Err("nvmlInit_v2 failed".into());
}
let mut dev = core::ptr::null_mut();
if by_index(0, &mut dev) != 0 {
return Err("no GPU 0".into());
}
let n = Nvml { dev, energy, power };
n.read()?;
Ok(n)
}
}
/// The cumulative energy, mJ.
pub fn read(&self) -> Result<u64, String> {
let mut v = 0u64;
// SAFETY: a device handle NVML gave, and a valid out-pointer.
let r = unsafe { (self.energy)(self.dev, &mut v) };
if r == 0 { Ok(v) } else { Err(format!("nvmlDeviceGetTotalEnergyConsumption returned {r}")) }
}
/// The board's power draw now, mW (read only outside the windows).
pub fn power_mw(&self) -> Option<u32> {
let mut v = 0u32;
// SAFETY: as above.
(unsafe { (self.power)(self.dev, &mut v) } == 0).then_some(v)
}
}
pub struct Meters {
pub rapl: Result<Rapl, String>,
pub nvml: Result<Nvml, String>,
}
pub struct Start {
t: Instant,
rapl: Option<u64>,
nvml: Option<u64>,
}
impl Meters {
pub fn open() -> Meters {
Meters { rapl: Rapl::open(), nvml: Nvml::open() }
}
pub fn describe(&self) -> String {
format!(
"rapl={} nvml={}",
self.rapl.as_ref().map_or_else(|e| format!("unavailable ({e})"), |_| "package-0".into()),
self.nvml.as_ref().map_or_else(|e| format!("unavailable ({e})"), |_| "gpu0 total energy".into())
)
}
pub fn start(&self) -> Start {
let nvml = self.nvml.as_ref().ok().and_then(|n| n.read().ok());
let rapl = self.rapl.as_ref().ok().and_then(|r| r.read().ok());
Start { t: Instant::now(), rapl, nvml }
}
/// `(seconds, package J, GPU board J)`.
pub fn finish(&self, s: Start) -> (f64, Option<f64>, Option<f64>) {
let rapl = self.rapl.as_ref().ok().and_then(|r| r.read().ok());
let nvml = self.nvml.as_ref().ok().and_then(|n| n.read().ok());
let secs = s.t.elapsed().as_secs_f64();
let pkg = match (&self.rapl, s.rapl, rapl) {
(Ok(r), Some(a), Some(b)) => Some(r.delta(a, b) as f64 / 1e6),
_ => None,
};
let gpu = match (s.nvml, nvml) {
(Some(a), Some(b)) if b >= a => Some((b - a) as f64 / 1e3),
_ => None,
};
(secs, pkg, gpu)
}
/// One idle window: `(package W, GPU board W, the board's draw now in W)`.
pub fn idle(&self, d: Duration) -> (Option<f64>, Option<f64>, Option<f64>) {
let s = self.start();
std::thread::sleep(d);
let (secs, pkg, gpu) = self.finish(s);
let draw = self.nvml.as_ref().ok().and_then(|n| n.power_mw()).map(|mw| mw as f64 / 1e3);
(pkg.map(|j| j / secs), gpu.map(|j| j / secs), draw)
}
}
}
// ---------------------------------------------------------------- energy, macOS
#[cfg(target_os = "macos")]
mod meter {
use jouleclaw_energy::process_gpu::GpuSnapshot;
use jouleclaw_energy::span::{ProcessSpanMeter, SpanEnergy, SpanSource, SpanStart, SpanUses};
pub struct Meters {
pub span: ProcessSpanMeter,
}
pub struct Start {
rail: Option<GpuSnapshot>,
span: SpanStart,
}
impl Meters {
/// The span meter; `gpu_load` puts some GPU work on the device from this process
/// for its liveness probe.
pub fn open(gpu_load: impl FnMut()) -> Meters {
Meters { span: ProcessSpanMeter::new(gpu_load) }
}
pub fn describe(&self) -> String {
format!("process span meter: metered={} fallback={}", self.span.is_metered(), self.span.fallback_name())
}
pub fn start(&self) -> Start {
let rail = self.span.gpu().snapshot().ok().filter(|s| s.rail_current);
Start { rail, span: self.span.start() }
}
/// The span's figure, and the GPU rail over the window: every GPU client's
/// energy and the GPU's idle draw, with this process's share of every client's
/// GPU time. The rail is an upper bound on this process's GPU energy, never its
/// figure.
pub fn finish(&self, s: Start) -> String {
let span = match self.span.finish(s.span, SpanUses::CpuGpu) {
Ok(e) => describe(&e),
Err(e) => format!("energy=none ({e})"),
};
let end = self.span.gpu().snapshot().ok().filter(|s| s.rail_current);
let rail = match (s.rail, end) {
(Some(a), Some(b)) => {
let j: u64 = b.rails_nj.iter().filter_map(|(n, e)| a.rails_nj.iter().find(|(m, _)| m == n).map(|(_, x)| e.saturating_sub(*x))).sum();
let pid = std::process::id() as i32;
let delta = |pick: &dyn Fn(i32) -> bool| -> u64 {
b.clients.iter().filter(|c| pick(c.pid)).map(|c| c.gpu_ns.saturating_sub(a.clients.iter().find(|x| x.id == c.id).map_or(0, |x| x.gpu_ns))).sum()
};
let (own, all) = (delta(&|p| p == pid), delta(&|_| true));
format!(" gpu_rail_window_j={:.4} own_gpu_time_share={:.3}", j as f64 / 1e9, if all > 0 { own as f64 / all as f64 } else { 0.0 })
}
_ => " gpu_rail_window_j=none".into(),
};
span + &rail
}
}
fn describe(e: &SpanEnergy) -> String {
match &e.source {
SpanSource::Process { cpu_uj, gpu } => format!(
"energy_j={:.4} process_cpu_j={:.4} process_gpu_j={:.4} provenance={:?} coverage={:?}",
e.uj as f64 / 1e6,
*cpu_uj as f64 / 1e6,
gpu.uj as f64 / 1e6,
e.provenance,
e.coverage
),
SpanSource::CpuOnly { cpu_uj, why } => format!("energy=partial process_cpu_j={:.4} process_gpu=none ({why}) coverage={:?}", *cpu_uj as f64 / 1e6, e.coverage),
SpanSource::Fallback { meter, why } => format!("energy=none: fell back to {meter} ({why})"),
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
mod meter {
pub struct Meters;
impl Meters {
pub fn start(&self) {}
}
}
fn percentile(sorted: &[f64], p: f64) -> f64 {
let i = ((sorted.len() - 1) as f64 * p).round() as usize;
sorted[i]
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let arg = |k: &str| args.iter().position(|a| a == k).and_then(|i| args.get(i + 1)).cloned();
let secs: f64 = arg("--secs").map_or(3.0, |s| s.parse().expect("--secs"));
let min_runs: usize = arg("--min-runs").map_or(5, |s| s.parse().expect("--min-runs"));
let only = arg("--only");
let d = wai_ops_gpu::blocking::discover();
for (a, why) in &d.refused {
println!("ops-gpu bench: adapter refused: key={} {} reason={why:?}", a.key(), a.describe());
}
let gpus: Vec<Arc<wai_ops_gpu::Gpu>> = d.found.into_iter().map(Arc::new).collect();
let cases: Vec<Case> = cases().into_iter().filter(|c| only.as_deref().is_none_or(|o| o == c.cap)).collect();
let threads = std::thread::available_parallelism().map_or(0, |n| n.get());
println!("ops-gpu bench: host_threads={threads} secs={secs} min_runs={min_runs} cases={} adapters={}", cases.len(), gpus.len());
#[cfg(target_os = "linux")]
let meters = {
let m = meter::Meters::open();
let (pkg, gpu, draw) = m.idle(std::time::Duration::from_secs_f64(secs));
println!(
"ops-gpu bench: meters: {} idle_package_w={} idle_gpu_board_w={} gpu_draw_now_w={}",
m.describe(),
pkg.map_or("-".into(), |w| format!("{w:.2}")),
gpu.map_or("-".into(), |w| format!("{w:.2}")),
draw.map_or("-".into(), |w| format!("{w:.2}"))
);
m
};
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let meters = meter::Meters;
#[cfg(target_os = "macos")]
let meters = {
let probe = cases.iter().find(|c| c.cap == "wai.neural.int_hyper").or(cases.first()).map(|c| &c.run);
let g = gpus.first().cloned();
let m = meter::Meters::open(|| {
if let (Some(g), Some(run)) = (&g, probe) {
for _ in 0..20 {
let _ = gpu::with_gpu(g, run);
}
}
});
println!("ops-gpu bench: meters: {}", m.describe());
m
};
for c in &cases {
let want = (c.run)();
let mut paths: Vec<(String, Option<Arc<wai_ops_gpu::Gpu>>)> = vec![("cpu".into(), None)];
for g in &gpus {
paths.push((format!("gpu:{}", g.adapter().key()), Some(Arc::clone(g))));
}
for (label, g) in paths {
let once = || -> (Vec<u8>, u64) {
match &g {
None => ((c.run)(), 0),
Some(g) => {
let (o, st) = gpu::with_gpu(g, || (c.run)());
assert!(st.errors.is_empty(), "{}: device errors {:?}", c.name, st.errors);
(o, st.on_gpu())
}
}
};
for _ in 0..2 {
assert!(once().0 == want, "{} {}: the output differs from the CPU's", c.cap, c.name);
}
let mut times = Vec::new();
let mut graphs = 0u64;
let t0 = Instant::now();
let start = meters.start();
while times.len() < min_runs || t0.elapsed().as_secs_f64() < secs {
let t = Instant::now();
let (o, n) = once();
times.push(t.elapsed().as_secs_f64() * 1e3);
graphs += n;
assert!(o == want, "{} {}: the output differs from the CPU's", c.cap, c.name);
}
#[cfg(target_os = "linux")]
let energy = {
let (w, pkg, board) = meters.finish(start);
let per = |j: Option<f64>| j.map_or("-".into(), |j| format!("{:.4}", j / times.len() as f64));
format!("window_s={w:.2} package_j_per_decode={} gpu_board_j_per_decode={}", per(pkg), per(board))
};
#[cfg(target_os = "macos")]
let energy = format!("window_s={:.2} {} (window totals, over runs={})", t0.elapsed().as_secs_f64(), meters.finish(start), times.len());
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let energy = {
let () = start;
"energy=none".to_string()
};
let mut s = times.clone();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!(
"ops-gpu bench: cap={} vector={} path={} runs={} graphs_on_gpu_per_decode={} median_ms={:.3} p10_ms={:.3} p90_ms={:.3} {energy}",
c.cap,
c.name,
label,
times.len(),
graphs / times.len() as u64,
percentile(&s, 0.5),
percentile(&s, 0.1),
percentile(&s, 0.9),
);
}
}
}