529 lines
25 KiB
Rust
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, ¶ms).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),
|
|
);
|
|
}
|
|
}
|
|
}
|