open-standards/wai/wai-ops-gpu/tests/mutants.rs
Transaction Science 040bd753e1 wai-ops-gpu: shader mutants the corpus must catch, and why the width is the certificate's
tests/mutants.rs runs the corpus on a device compiled from an edited kernel:
a rounding shift or its addend off by one, a tie rule flipped, a clamp bound
moved by one, a check that misses values above its range, a dropped carry or
cross term of a product, a sign rule, a post bias dropped, an index rule of
the transposed convolution, the convolution's padding edge, igdn's term
floor, the leaky shift of 32 or more, and two data-movement orders. Each must
give a result that differs from the committed one.

tests/corpus.rs also runs the 32-bit kernels on the graphs whose
certificate does not meet acc32, through a hidden entry point (run_at
refuses them): some differ from the interpreter, so the width a graph runs
at is load-bearing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-20 22:46:56 -04:00

95 lines
5.9 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The corpus catches an edited kernel. Each mutant below changes one expression of
//! one shader module, the kind of slip an integer kernel makes: a rounding shift off by
//! one, a wrong saturation bound, a dropped carry or cross term, a sign rule, an index
//! rule. A device running the mutant must give at least one result of the corpus
//! (`tests/corpus.rs`) that differs from the committed one. The arithmetic probe is left
//! as shipped, so the device opens and the corpus alone has to catch the edit.
//!
//! Each adapter's result is one line, `ops-gpu mutants: key=… killed N of N`, appended
//! to `WAI_OPS_GPU_REPORT` when it is set.
#[path = "support/corpus.rs"]
mod corpus;
use std::io::Write;
use wai_ops_gpu::{blocking, kernels::Sources};
#[derive(Clone, Copy)]
enum Module {
Ops32,
Ops64,
Move,
}
/// `(name, module, the text, its replacement)`; the text must occur in the module.
const MUTANTS: &[(&str, Module, &str, &str)] = &[
("half_up addend off by one bit (64-bit rounding, acc32)", Module::Ops32, "let h = w_pow2(s - 1u);", "let h = w_pow2(s);"),
("half_up addend off by one bit (128-bit rounding, acc64)", Module::Ops64, "let h = q_pow2(s - 1u);", "let h = q_pow2(s);"),
("rounding shift one short (acc64)", Module::Ops64, "return q_sar(q_add(p, h), s);", "return q_sar(q_add(p, h), s - 1u);"),
("half_even ties to odd (acc64)", Module::Ops64, "(q_eq(rem, h) && (q.x & 1u) == 1u)", "(q_eq(rem, h) && (q.x & 1u) == 0u)"),
("half_even ties to odd (acc32)", Module::Ops32, "(w_eq(rem, h) && (q.x & 1u) == 1u)", "(w_eq(rem, h) && (q.x & 1u) == 0u)"),
("clamp operator saturates one below its top (acc64)", Module::Ops64, "put64(t, w_clamp(a64(t), lo, hi));", "put64(t, w_clamp(a64(t), lo, w_sub(hi, W(1u, 0u))));"),
("clamp policy saturates one above its bottom (acc32)", Module::Ops32, "O[t] = bitcast<u32>(clamp(v, lo, hi));", "O[t] = bitcast<u32>(clamp(v, lo + 1, hi));"),
("check policy misses values above its range (acc64)", Module::Ops64, "} else if (w_lt(v, lo) || w_lt(hi, v)) {", "} else if (w_lt(v, lo)) {"),
// In the 32-bit kernels the 32 × 32-bit product only ever sees magnitudes of at most
// 2^31, for which neither carry of its middle sum can be set (a0·b1 + a1·b0 stays
// below 2^32 − 2^17), so dropping one there is an equivalent mutant: it is dropped
// in the 64-bit kernels, whose products take full 32-bit words.
("product's middle carry dropped (acc64)", Module::Ops64, "let hi = p11 + (mid2 >> 16u) + ((c1 + c2) << 16u);", "let hi = p11 + (mid2 >> 16u) + (c2 << 16u);"),
("64-bit product's cross term dropped (acc64)", Module::Ops64, "return W(p.x, p.y + a.x * b.y + a.y * b.x);", "return W(p.x, p.y + a.x * b.y);"),
("signed product's sign rule (acc32)", Module::Ops32, "return select(m, w_neg(m), na != nb);", "return select(m, w_neg(m), na);"),
("post bias dropped (acc32)", Module::Ops32, "y = w_add(y, w_sext(m32(P.b_off + 2u * o)));", "y = y;"),
("transposed convolution's residue (acc64)", Module::Ops64, "for (var i = yy % P.sh; i < P.kh && i <= yy; i += P.sh) {", "for (var i = (yy + 1u) % P.sh; i < P.kh && i <= yy; i += P.sh) {"),
("convolution's bottom padding edge (acc32)", Module::Ops32, "if (iy < P.ph || iy - P.ph >= P.h) {", "if (iy < P.ph || iy - P.ph > P.h) {"),
("igdn term floored one bit further (acc64)", Module::Ops64, "n = q_add(n, q_sar(gx2, P.f));", "n = q_add(n, q_sar(gx2, P.f + 1u));"),
("leaky shift of 32 or more (acc32)", Module::Ops32, "if (s >= 32u) {\n return bitcast<u32>(bitcast<i32>(a) >> 31u);", "if (s > 32u) {\n return bitcast<u32>(bitcast<i32>(a) >> 31u);"),
("pixel_shuffle's sub-position order", Module::Move, "(y % r) * r + x % r", "(x % r) * r + y % r"),
("subpos_merge's column offset", Module::Move, "copy_el((c * P.ho + 2u * y + P.k0) * P.wo + 2u * x + P.k1, t);", "copy_el((c * P.ho + 2u * y + P.k1) * P.wo + 2u * x + P.k0, t);"),
];
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();
}
}
#[test]
fn every_mutant_is_caught_on_every_adapter() {
let d = blocking::discover();
if d.found.is_empty() {
report("ops-gpu mutants: 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 keys: Vec<String> = d.found.iter().map(|g| g.adapter().key()).collect();
let vs = corpus::vectors();
let mut killed = vec![0usize; keys.len()];
for &(name, module, from, to) in MUTANTS {
let mut src = Sources::shipped();
let text = match module {
Module::Ops32 => &mut src.ops32,
Module::Ops64 => &mut src.ops64,
Module::Move => &mut src.mv,
};
assert!(text.contains(from), "mutant {name:?}: its text is not in the module");
*text = text.replace(from, to);
let m = blocking::discover_with(&src, name);
assert_eq!(m.found.len(), keys.len(), "mutant {name:?}: the adapters did not all open: {:?}", m.refused.iter().map(|(_, w)| w).collect::<Vec<_>>());
for g in &m.found {
let k = keys.iter().position(|x| *x == g.adapter().key()).expect("the same adapters");
let t = corpus::run_all(g, &vs, 1);
let caught = !t.mismatches.is_empty() || !t.errors.is_empty();
println!("ops-gpu mutant: key={} {name:?}: {}", keys[k], t.mismatches.first().map_or_else(|| "SURVIVED".to_string(), |m| format!("caught: {m}")));
killed[k] += caught as usize;
}
}
for (k, key) in keys.iter().enumerate() {
report(&format!("ops-gpu mutants: key={key} killed {} of {}", killed[k], MUTANTS.len()));
}
for (k, key) in keys.iter().enumerate() {
assert_eq!(killed[k], MUTANTS.len(), "{key}: a mutant survived");
}
}