A new crate, wai/wai-ops-npu, with no dependency but wai-ops. Its kernel runs on one compute core of a tile array, built at run time with the array's open toolchain; its host side is compiled against the device runtime and loaded. The interpreter runs each graph through wai_ops::run_with and asks the core for every arithmetic operator's outputs: conv and conv_transpose as multiply- accumulate lines (8- and 16-bit operands into 32-bit accumulator lanes, 32-bit operands into 64-bit lanes, the width the certificate's mac record allows), rescale, chscale and mul through the unit's shift-round step under the mode that is the operator's rounding, the elementwise operators, and lut16 a table block at a time. Movement, policies, checks and state stay the interpreter's. The unit's shift-round step is exact for shifts 0..59 only: it takes 60..63 as a left shift by 64 - s (measured), so those shifts go through the core's scalar unit in 64-bit arithmetic. Tests: the corpus (every graph vector in the subset, against the committed results and the interpreter), the rounding, saturation, lane and requantisation probes, twelve kernel edits the corpus must catch, and the graphs outside the subset by acc32 alone, which are not exact here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
49 lines
2.3 KiB
Rust
49 lines
2.3 KiB
Rust
//! Each edit of the kernel (`MUT` in `kernel/ops_npu.cc`, [`Variant::MUTANTS`]) is its
|
|
//! own program, built and run on the device; the conformance corpus must catch every
|
|
//! one, or, for the one edit no committed graph reaches, the requantisation probe must.
|
|
//! A result equal to the interpreter's under an edited kernel would mean the device did
|
|
//! not compute it.
|
|
//!
|
|
//! With no device the test prints `ops-npu mutants: SKIPPED`, and fails under
|
|
//! `WAI_OPS_NPU_REQUIRE`.
|
|
|
|
#[path = "support/corpus.rs"]
|
|
mod corpus;
|
|
#[path = "support/rq.rs"]
|
|
mod rq;
|
|
|
|
use corpus::report;
|
|
use wai_ops_npu::Variant;
|
|
|
|
#[test]
|
|
fn every_edit_of_the_kernel_is_caught() {
|
|
let Some(npu) = corpus::device("mutants") else { return };
|
|
let vs = corpus::vectors();
|
|
let (rs, _) = corpus::ready(&vs);
|
|
let inside: Vec<&corpus::Ready<'_>> = rs.iter().filter(|r| r.plan.in_subset()).collect();
|
|
let mut caught = 0;
|
|
for (v, what) in Variant::MUTANTS {
|
|
let m = npu.with_variant(v).unwrap_or_else(|e| panic!("{}: {e}", v.label()));
|
|
let t = corpus::run_all(&m, &inside, usize::MAX);
|
|
assert!(t.stats.errors.is_empty(), "{}: device errors: {:?}", v.label(), t.stats.errors.first());
|
|
let by = if t.mismatched > 0 {
|
|
// Which corpus files the differing vectors come from.
|
|
let mut files: std::collections::BTreeMap<String, u32> = Default::default();
|
|
for m in &t.mismatches {
|
|
if let Some(f) = m.split(" (").nth(1).and_then(|r| r.split(')').next()) {
|
|
*files.entry(f.to_string()).or_default() += 1;
|
|
}
|
|
}
|
|
format!("the corpus ({} vectors differ, by file {files:?}; the first: {})", t.mismatched, t.mismatches[0])
|
|
} else {
|
|
let (bad, first) = rq::run(&m, 1, 0x6d75_7461_6e74);
|
|
if bad > 0 { format!("the requantisation probe only ({bad} cases differ; the first: {first})") } else { String::new() }
|
|
};
|
|
report(&format!("ops-npu mutant: key={} {} ({what}): {}", npu.key(), v.label(), if by.is_empty() { "SURVIVED" } else { &by }));
|
|
if !by.is_empty() {
|
|
caught += 1;
|
|
}
|
|
}
|
|
report(&format!("ops-npu mutants: key={} killed {caught} of {}", npu.key(), Variant::MUTANTS.len()));
|
|
assert_eq!(caught, Variant::MUTANTS.len(), "an edited kernel gave the interpreter's results");
|
|
}
|