open-standards/wai/wai-rs/tests/ops_gpu.rs
Transaction Science bb9e117fc1 wai-rs: ops_gpu compares each graph the device runs with the interpreter, under with_gpu_compared
The capability test now holds the device to the interpreter graph by graph,
as well as through each decode's bytes: every graph the device ran was
compared, and none differed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 04:40:10 -04:00

386 lines
18 KiB
Rust

//! The integer decoders with their graphs on a GPU (`int_ops::gpu`, `wai_ops_gpu`): every
//! committed vector and refusal fixture of every capability whose decode runs a
//! `wai-ops/1` graph decodes to the same bytes, or is refused with the same code, as the
//! decode whose graphs run on the CPU, on every adapter this process finds.
//!
//! - `int-payload-conformance/vectors.json`: its payload vectors and refusal fixtures
//! (`wai.neural.int_hyper`, `wai.video.int_hyper`, `wai.audio.int_codec`,
//! `wai.splat.int_codec`; `wai.video.int_motion` runs no graph, and is listed with no
//! graph runs);
//! - `neural-conformance/`: the `wai.neural.int_synth` vectors;
//! - `wai-rs/tests/vectors/int_mlicpp`: the `wai.neural.int_mlicpp` vector;
//! - `wai-rs/tests/vectors/int_world{,2}`: the integer renderer of `wai.world.model`
//! (informative, integer-operators §13.5);
//! - `wai.image.jpegai`'s exact decoder on the `jpegai/v1` inputs, when
//! `WAI_JPEGAI_CODESTREAM`, `WAI_JPEGAI_WEIGHTS_DIR` and `WAI_JPEGAI_NORM` name them
//! (`tools/wai_jpegai_fetch_e2e_inputs.sh`). Its scale graphs and hyper decoders run on
//! the device; its synthesis graphs use operators outside `wai-ops/1-i32` and run on the
//! CPU, which the line for it states.
//!
//! Each graph the device runs is also run on the interpreter, and its outputs compared
//! (`gpu::with_gpu_compared`): so the equality is shown graph by graph, and not only
//! through each decode's final bytes.
//!
//! **A skip is not a pass.** With no adapter the test prints `ops-gpu capabilities:
//! SKIPPED` and fails when `WAI_OPS_GPU_REQUIRE` is set; without the `jpegai/v1` inputs it
//! prints that `wai.image.jpegai` was not run, and fails when `WAI_REQUIRE_SOTA_VECTORS`
//! is set. A capability whose decodes ran no graph on the device fails. Each adapter's
//! result is one line per capability, `ops-gpu capability: key=…`, appended to
//! `WAI_OPS_GPU_REPORT` when it is set.
#![cfg(all(feature = "ops_gpu", feature = "video_int", feature = "audio_int", feature = "splat_int", feature = "world_int"))]
use std::collections::BTreeMap;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use serde_json::Value;
use wai::int_hyper_synth::IntModel;
use wai::int_ops::gpu::{self, Stats, wai_ops_gpu};
use wai::int_payload::{IntDecoded, IntParams, decode_int};
use wai::pin::HeldBytes;
fn wai() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn read(rel: &str) -> Vec<u8> {
std::fs::read(wai().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}"))
}
fn report(line: &str) {
println!("{line}");
if let Ok(p) = std::env::var("WAI_OPS_GPU_REPORT") {
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&p).unwrap();
writeln!(f, "{line}").unwrap();
}
}
/// A decode's result as bytes: every output byte the decode gives, or its refusal code.
type Out = Result<Vec<u8>, String>;
fn bytes_of(d: &IntDecoded) -> Vec<u8> {
let mut b = Vec::new();
match d {
IntDecoded::Image { rgb, width, height } => {
b.extend_from_slice(&width.to_le_bytes());
b.extend_from_slice(&height.to_le_bytes());
b.extend_from_slice(rgb);
}
IntDecoded::Video { frames, width, height } => {
b.extend_from_slice(&width.to_le_bytes());
b.extend_from_slice(&height.to_le_bytes());
for f in frames {
b.extend_from_slice(f);
}
}
IntDecoded::Pcm { samples, sample_rate, channels } => {
b.extend_from_slice(&sample_rate.to_le_bytes());
b.extend_from_slice(&channels.to_le_bytes());
for s in samples {
b.extend_from_slice(&s.to_le_bytes());
}
}
IntDecoded::Attr { bytes, n_attr, s, n_gaussians, ranges } => {
for v in [n_attr, s, n_gaussians] {
b.extend_from_slice(&v.to_le_bytes());
}
for (lo, hi) in ranges {
b.extend_from_slice(&lo.to_bits().to_le_bytes());
b.extend_from_slice(&hi.to_bits().to_le_bytes());
}
b.extend_from_slice(bytes);
}
_ => panic!("an output form this test does not compare"),
}
b
}
/// One decode of the corpus: its capability, its name, and the decode.
struct Case {
cap: &'static str,
name: String,
refusal: bool,
run: Box<dyn Fn() -> Out>,
}
fn cap_static(c: &str) -> &'static str {
for k in [
"wai.video.int_motion", "wai.neural.int_hyper", "wai.video.int_hyper", "wai.audio.int_codec", "wai.splat.int_codec", "wai.neural.int_synth",
"wai.neural.int_mlicpp", "wai.world.model", "wai.image.jpegai",
] {
if k == c {
return k;
}
}
panic!("an unknown capability {c}")
}
fn payload_cases() -> Vec<Case> {
let idx = json("int-payload-conformance/vectors.json");
let mut out = Vec::new();
for (list, refusal) in [("vectors", false), ("refusals", true)] {
for e in idx[list].as_array().unwrap() {
let cap = cap_static(e["capability"].as_str().unwrap());
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 || -> Out {
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());
decode_int(cap, &payload, &params).map(|d| bytes_of(&d)).map_err(|e| e.code().to_string())
};
out.push(Case { cap, name: e["name"].as_str().unwrap().to_string(), refusal, run: Box::new(run) });
}
}
out
}
fn synth_cases() -> Vec<Case> {
let hashes = json("neural-conformance/expected_hashes.json");
hashes
.as_object()
.unwrap()
.keys()
.map(|name| {
let dir = format!("neural-conformance/vectors/{name}");
let (payload, set) = (read(&format!("{dir}/latent.bin")), read(&format!("{dir}/int_synth.json")));
let run = move || -> Out { wai::int_synth::decode_from_spec(&payload, &set).map(|i| [i.width.to_le_bytes().to_vec(), i.height.to_le_bytes().to_vec(), i.rgb].concat()).map_err(|e| e.to_string()) };
Case { cap: "wai.neural.int_synth", name: name.clone(), refusal: false, run: Box::new(run) }
})
.collect()
}
fn mlicpp_cases() -> Vec<Case> {
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;
let run = move || -> Out { wai::int_mlicpp::decode(&m, &p).map(|i| [(i.width as u64).to_le_bytes().to_vec(), (i.height as u64).to_le_bytes().to_vec(), i.rgb].concat()).map_err(|e| format!("{e:?}")) };
vec![Case { cap: "wai.neural.int_mlicpp", name: "int_mlicpp".into(), refusal: false, run: Box::new(run) }]
}
fn world_cases() -> Vec<Case> {
let mut out = Vec::new();
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 = j["c"].as_u64().unwrap() as usize;
let lat = 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(),
};
let run = move || -> Out { wai::int_world::decode_planes(&tm, &gm, &z0, &rows, c, lat).map(|w| w.frames.concat()).map_err(|e| format!("{e:?}")) };
out.push(Case { cap: "wai.world.model", name: name.into(), refusal: false, run: Box::new(run) });
}
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,
}
}
/// The `jpegai/v1` inputs, as `tests/jpegai_e2e_full.rs` loads them.
pub fn load(cs: &str, dir: &str, nf: &str) -> (Vec<u8>, DecoderWeights, NormTables) {
let data = std::fs::read(cs).unwrap_or_else(|e| panic!("read {cs}: {e}"));
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)
}
}
fn jpegai_cases() -> (Vec<Case>, Option<String>) {
let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty());
let (Some(cs), Some(dir), Some(nf)) = (env("WAI_JPEGAI_CODESTREAM"), env("WAI_JPEGAI_WEIGHTS_DIR"), env("WAI_JPEGAI_NORM")) else {
return (vec![], Some("WAI_JPEGAI_CODESTREAM, WAI_JPEGAI_WEIGHTS_DIR and WAI_JPEGAI_NORM are not all set (tools/wai_jpegai_fetch_e2e_inputs.sh)".into()));
};
let (data, weights, norm) = jpegai::load(&cs, &dir, &nf);
let run = move || -> Out { wai::jpegai_decode::decode(&data, &weights, &norm).map(|i| [(i.h as u64).to_le_bytes().to_vec(), (i.w as u64).to_le_bytes().to_vec(), i.data.clone()].concat()).map_err(|r| format!("{:?}", r.code())) };
(vec![Case { cap: "wai.image.jpegai", name: "jpegai/v1 test256".into(), refusal: false, run: Box::new(run) }], None)
}
/// What one capability's cases did on one adapter.
#[derive(Default)]
struct Tally {
cases: u32,
refusals: u32,
stats: Stats,
differ: Vec<String>,
}
#[test]
fn every_capability_decodes_alike_with_its_graphs_on_every_adapter() {
let d = wai_ops_gpu::blocking::discover();
for (a, why) in &d.refused {
report(&format!("ops-gpu adapter refused: key={} {} reason={why:?}", a.key(), a.describe()));
}
if d.found.is_empty() {
report("ops-gpu capabilities: SKIPPED: no adapter");
assert!(std::env::var_os("WAI_OPS_GPU_REQUIRE").is_none(), "WAI_OPS_GPU_REQUIRE is set and no adapter was found");
return;
}
let mut cases = payload_cases();
cases.extend(synth_cases());
cases.extend(mlicpp_cases());
cases.extend(world_cases());
let (jp, missing) = jpegai_cases();
cases.extend(jp);
if let Some(why) = &missing {
report(&format!("ops-gpu capability: wai.image.jpegai NOT RUN: {why}"));
assert!(std::env::var_os("WAI_REQUIRE_SOTA_VECTORS").is_none(), "WAI_REQUIRE_SOTA_VECTORS is set and the jpegai/v1 inputs are absent");
}
// The CPU's result for every case, once.
let want: Vec<Out> = cases.iter().map(|c| (c.run)()).collect();
for (c, w) in cases.iter().zip(&want) {
assert_eq!(c.refusal, w.is_err(), "{} {}: a vector decodes and a refusal fixture is refused, on the CPU: {:?}", c.cap, c.name, w.as_ref().err());
}
for g in d.found.into_iter().map(Arc::new) {
let a = g.adapter().clone();
let mut by_cap: BTreeMap<&'static str, Tally> = BTreeMap::new();
for (c, w) in cases.iter().zip(&want) {
let (got, stats) = gpu::with_gpu_compared(&g, || (c.run)());
let t = by_cap.entry(c.cap).or_default();
t.cases += 1;
t.refusals += c.refusal as u32;
if &got != w {
t.differ.push(format!("{}: {}", c.name, match (w, &got) {
(Ok(x), Ok(y)) => format!("{} bytes vs {} bytes, first difference at {:?}", x.len(), y.len(), x.iter().zip(y).position(|(p, q)| p != q)),
_ => format!("{:?} vs {:?}", w.as_ref().map(|v| v.len()), got.as_ref().map(|v| v.len())),
}));
}
t.stats.acc32 += stats.acc32;
t.stats.acc64 += stats.acc64;
for (k, n) in stats.cpu {
*t.stats.cpu.entry(k).or_default() += n;
}
t.stats.errors.extend(stats.errors);
t.stats.compared += stats.compared;
t.stats.differed += stats.differed;
}
for (cap, t) in &by_cap {
let cpu: Vec<String> = t.stats.cpu.iter().map(|(k, n)| format!("{n}x {k}")).collect();
report(&format!(
"ops-gpu capability: key={} {} cap={cap} cases={} refusals={} graphs_gpu_acc32={} graphs_gpu_acc64={} graphs_cpu={} [{}] graphs_compared={} graphs_differ={} differ={} errors={}",
a.key(),
a.describe(),
t.cases,
t.refusals,
t.stats.acc32,
t.stats.acc64,
t.stats.on_cpu(),
cpu.join("; "),
t.stats.compared,
t.stats.differed,
t.differ.len(),
t.stats.errors.len()
));
}
for (cap, t) in &by_cap {
assert!(t.differ.is_empty(), "{} {cap}: {:?}", a.key(), t.differ);
assert!(t.stats.errors.is_empty(), "{} {cap}: device errors {:?}", a.key(), t.stats.errors);
assert_eq!(t.stats.differed, 0, "{} {cap}: a graph's outputs on the device differ from the interpreter's", a.key());
assert_eq!(t.stats.compared, t.stats.on_gpu(), "{} {cap}: every graph the device ran is compared", a.key());
if *cap != "wai.video.int_motion" {
assert!(t.stats.on_gpu() > 0, "{} {cap}: no graph ran on the device", a.key());
} else {
assert_eq!(t.stats.on_gpu() + t.stats.on_cpu(), 0, "{} {cap}: int_motion runs no graph", a.key());
}
if *cap != "wai.image.jpegai" {
assert_eq!(t.stats.on_cpu(), 0, "{} {cap}: every graph of this capability is in wai-ops/1-i32", a.key());
}
}
}
}