Files
lightcraft/crates/denoise/src/cpu/tests.rs
T
7935999d94 LR-EDIT-DETAIL-DENOISE: non-destructive AI RAW denoise with pure-Rust CPU/GPU
Recover the unpublished denoise work against current upstream. Preserve later opt-in model setup, shared Rust HTTPS downloads, pending-photo resume and an Amount-independent switch. Keep original RAWs unchanged and share the current color finish between ordinary and denoised sources.

Bound public tiling inputs, refuse corrupt-cache exports, and retain synthetic/real-model coverage. Include two Windows path spelling fixes in existing tests. No model weights, face code, media or Tract runtime are included.

Validation: cargo xtask ci (all seven gates); installed RawNIND CPU/GPU agreement 1.38e-6; native CPU preview/export on a public 24 MP Sony RAW, unchanged original SHA-256.

Co-authored-by: Codex <noreply@openai.com>

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

Generated with Codex.
2026-10-08 22:07:20 -04:00

152 lines
5.9 KiB
Rust

use super::*;
use crate::{
net::{Conv, ConvT2},
reference, synthetic,
};
fn random(n: usize, seed: u32) -> Vec<f32> {
let mut s = seed | 1;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
(s >> 8) as f32 / (1u32 << 23) as f32 - 1.0
})
.collect()
}
fn net(tile: usize, ch: u64, depth: usize, seed: u64) -> Net {
let p = std::env::temp_dir().join(format!("lc-purecpu-{}-{tile}-{ch}-{depth}-{seed}.onnx", std::process::id()));
std::fs::write(&p, synthetic::unet_onnx(tile as u64, ch, depth, seed)).unwrap();
let n = crate::onnx::read(&p).unwrap();
std::fs::remove_file(p).unwrap();
n
}
fn agree(a: &[f32], b: &[f32]) {
assert_eq!(a.len(), b.len());
assert!(a.iter().chain(b).all(|v| v.is_finite()));
let scale = b.iter().fold(1e-6f32, |m, v| m.max(v.abs()));
let error = a.iter().zip(b).fold(0f32, |m, (a, b)| m.max((a - b).abs())) / scale;
assert!(error <= 1e-3, "relative maximum error {error:e}");
}
#[test]
fn all_operators_match_scalar_on_random_nets_and_odd_and_tiny_shapes() {
for (t, ch, d) in [(1, 3, 0), (3, 5, 0), (5, 7, 0), (6, 3, 1), (20, 5, 2), (36, 9, 2), (64, 8, 3)] {
for seed in [1, 73] {
let n = net(t, ch, d, seed);
let r = NetRunner::new(&n, t).unwrap();
let x = random(4 * t * t, seed as u32);
let want = reference::run(&n, t, &x).unwrap();
agree(&r.run(&x).unwrap(), &want);
assert_eq!(r.run(&x).unwrap(), r.run(&x).unwrap());
}
}
}
#[test]
fn malformed_values_fail_before_execution_and_workspace_sizes_are_bounded() {
let mut n = net(4, 3, 1, 11);
for t in [0, 1, 3, usize::MAX, 4096] {
assert!(NetRunner::new(&n, t).is_err());
}
for ch in [0, 3, usize::MAX] {
n.in_channels = ch;
assert!(NetRunner::new(&n, 4).is_err());
}
let n = Net::new(
4,
vec![
Op::Conv(Conv { src: 0, src2: None, cin: 4, cout: 4096, k: 1, weight: vec![0.1; 4 * 4096], bias: vec![0.0; 4096], leaky: None }),
Op::Conv(Conv { src: 1, src2: None, cin: 4096, cout: 12, k: 1, weight: vec![0.1; 4096 * 12], bias: vec![0.0; 12], leaky: None }),
Op::DepthToSpace2 { src: 2 },
],
)
.unwrap();
assert!(NetRunner::new(&n, 2048).is_err());
let ops = (0..100).map(|i| Op::MaxPool2 { src: i }).collect();
let n = Net::new(4, ops).unwrap();
assert!(NetRunner::new(&n, 16).is_err());
let c = Conv { src: 0, src2: None, cin: 4, cout: 12, k: 1, weight: vec![0.25; 48], bias: vec![0.0; 12], leaky: None };
for mode in 0..6 {
let mut bad = c.clone();
match mode {
0 => bad.src = usize::MAX,
1 => bad.weight.pop().map(|_| ()).unwrap(),
2 => bad.bias[0] = f32::NAN,
3 => bad.leaky = Some(f32::INFINITY),
4 => bad.cout = usize::MAX,
_ => bad.k = usize::MAX,
}
assert!(Net::new(4, vec![Op::Conv(bad), Op::DepthToSpace2 { src: 1 }]).is_err());
}
}
#[test]
fn bad_tiles_and_arithmetic_overflow_are_errors_and_do_not_poison_reuse() {
let c = Conv { src: 0, src2: None, cin: 4, cout: 12, k: 1, weight: vec![f32::MAX; 48], bias: vec![0.0; 12], leaky: None };
let n = Net::new(4, vec![Op::Conv(c), Op::DepthToSpace2 { src: 1 }]).unwrap();
let r = NetRunner::new(&n, 1).unwrap();
assert!(r.run(&[]).is_err());
assert!(r.run(&[f32::NAN; 4]).is_err());
assert!(r.run(&[f32::INFINITY; 4]).is_err());
assert_eq!(r.idle.lock().unwrap().len(), 0, "invalid input never allocates a workspace");
assert!(r.run(&[2.0; 4]).is_err());
assert_eq!(r.run(&[0.0; 4]).unwrap(), vec![0.0; 12]);
// Matrix shape checks are the boundary in front of the dependency's asserting constructors.
assert!(multiply(&[], &[], &mut [], usize::MAX, 2, 2).is_err());
}
#[test]
fn shared_runner_serves_concurrent_tiles_without_cross_contamination() {
let n = net(12, 5, 1, 97);
let r = NetRunner::new(&n, 12).unwrap();
let inputs: Vec<_> = (0..8).map(|s| random(4 * 12 * 12, s)).collect();
let answers: Vec<_> = inputs.iter().map(|x| reference::run(&n, 12, x).unwrap()).collect();
std::thread::scope(|scope| {
let mut handles = Vec::new();
for (x, want) in inputs.iter().zip(&answers) {
let r = &r;
handles.push(scope.spawn(move || {
for _ in 0..3 {
agree(&r.run(x).unwrap(), want);
}
}));
}
for h in handles {
h.join().unwrap();
}
});
}
#[test]
fn join_may_read_the_same_tensor_twice_and_leaky_up_convolution_is_supported() {
let c = ConvT2 { src: 0, cin: 4, cout: 3, weight: random(48, 2), bias: vec![0.1, -0.1, 0.0], leaky: Some(0.13) };
let n = Net::new(
4,
vec![
Op::ConvT2(c),
Op::Conv(Conv { src: 1, src2: Some(1), cin: 6, cout: 3, k: 3, weight: random(162, 3), bias: vec![0.0; 3], leaky: Some(0.0) }),
],
)
.unwrap();
let x = random(4 * 3 * 3, 7);
agree(&NetRunner::new(&n, 3).unwrap().run(&x).unwrap(), &reference::run(&n, 3, &x).unwrap());
}
#[test]
fn reused_im2col_clears_all_edge_padding_between_images() {
for t in [1, 3, 5, 36] {
let n = net(t, 5, 0, 121);
let r = NetRunner::new(&n, t).unwrap();
for x in [vec![1.0; 4 * t * t], vec![0.0; 4 * t * t], random(4 * t * t, 9)] {
agree(&r.run(&x).unwrap(), &reference::run(&n, t, &x).unwrap());
}
}
}
#[test]
#[ignore = "needs LC_DENOISE_MODEL; one tile with LIGHTCRAFT_PROFILE=1"]
fn profile_one_real_tile() {
let model = std::env::var_os("LC_DENOISE_MODEL").expect("set LC_DENOISE_MODEL");
let m = crate::known::find("rawnind-bayer").unwrap().manifest;
let r = crate::runtime::CpuRunner::load(std::path::Path::new(&model), &m).unwrap();
println!("workspace {:.1} MB", r.bytes_per_tile() as f64 / 1e6);
r.run(&crate::runtime::check_tile(512)).unwrap();
}