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]>
95 lines
5.9 KiB
Rust
95 lines
5.9 KiB
Rust
//! 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");
|
||
}
|
||
}
|