//! 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(clamp(v, lo, hi));", "O[t] = bitcast(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(bitcast(a) >> 31u);", "if (s > 32u) {\n return bitcast(bitcast(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 = 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::>()); 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"); } }