open-standards/wai/wai-rs/tests/int_kernels_geometry.rs
Transaction Science 9474a402d8 D2c M3 gate 7: the audio decode's direct guard and fast dispatch go
- int_audio: the direct path's fast dispatch (conv_t_1d over int_kernels) is deleted;
  the test-only direct decode runs each transposed convolution on its scalar kernel
  (reference::conv_t_1d, verbatim, itself test-only now). Its guard layer_in_range,
  and run_layer, live only inside the test-only legacy module: the production audio
  decode's exactness checks are its graph's check ranges, the PCM step's included.
- The kernel_equivalence module (the dispatch against the scalar kernel; retargeted at
  the interpreter at gate 5) is deleted. Its edge geometry moves into int_audio's
  geometry tests, through the lowered graph under every policy: a one-sample input
  upsampled by any stride a WAM1 field holds; several samples whose uncropped length
  is past the cap (too_large, the fast kernel never entered); padding past the fast
  kernels' pad factor (the interpreter computes it); an input that could overflow,
  refused by the check range before any arithmetic.
- int_hyper_synth's docs: igdn_check's audio caller and run_net_checked's are
  test-only now; the conditional codec's checked decode is the last production
  caller (M4). int_kernels_geometry's and the export tool's notes follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 20:25:19 -04:00

303 lines
15 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.

//! Geometry at the edges of what the fast integer kernels lay out, run through the
//! public API under the scalar reference kernels AND under each fast-kernel policy
//! (serial, explicit multi-threaded tilings, automatic):
//!
//! * a 1×1 input upsampled by strides from 1 up to `usize::MAX`, through the
//! transposed convolutions of int_hyper (`run_net`), int_synth
//! (`decode_from_spec`) and the JPEG-AI synthesis (`conv_transpose`);
//! * a regular convolution whose stride reaches or exceeds its padded input
//! (`run_net`, `conv_grouped`);
//! * padding far beyond the kernel, with output padding restoring a small output;
//! * an input of two or more rows and columns whose stride and padding make the
//! reference's own size arithmetic overflow (int_hyper, int_synth, JPEG-AI).
//!
//! The reference computes each of the first three in a handful of operations. Every
//! policy must return the same integers (checked against a hand computation), and
//! the fast path must not overflow, or allocate or loop in proportion to the stride
//! or the padding, where the reference does not. In the last, the reference panics,
//! and every policy must do the same. int_synth decodes through its lowering
//! (integer-operators §13.1), whose fields are 32 bits and whose run-time plan caps
//! every size it forms at 2^26: where a case is past either, every policy refuses it,
//! one of the planned breaks of integer-operators §15.3. int_audio decodes through its
//! lowering `wam1/1` too; its 1-D transposed convolution's edge geometry is tested
//! through that graph under every policy (`int_audio` unit tests). CI runs this file
//! beside `int_kernels_vectors` on every `wai-conformance` leg, with overflow checks
//! on and off.
#![cfg(feature = "neural_int")]
use serde_json::json;
use wai::int_kernels::{with_policy, Policy};
const POLICIES: [Policy; 5] = [Policy::reference(), Policy::fast(1), Policy::fast(2), Policy::fast(5), Policy::fast(0)];
/// A 3×3 kernel `[[1, 2, 3], [4, 5, 6], [7, 8, 9]]`.
const KERNEL: [i64; 9] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/// Run under every policy; all results must be identical. Returns the result.
fn same_under_every_policy<T: PartialEq + std::fmt::Debug>(what: &str, f: impl Fn() -> T) -> T {
let want = with_policy(POLICIES[0], &f);
for p in &POLICIES[1..] {
let got = with_policy(*p, &f);
assert!(got == want, "{what}: result under {p:?} differs from the reference kernels ({got:?} vs {want:?})");
}
want
}
/// Run under every policy, catching a panic. The outcome (the value, or that the
/// call panicked) must be the same under every policy. Returns the reference
/// kernels' outcome, `None` for a panic. The panic message is not compared: a fast
/// policy may fail at another element first, or on a worker thread.
fn same_outcome_under_every_policy<T: PartialEq + std::fmt::Debug>(what: &str, f: impl Fn() -> T) -> Option<T> {
let run = |p: Policy| std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| with_policy(p, &f))).ok();
let want = run(POLICIES[0]);
for p in &POLICIES[1..] {
let got = run(*p);
assert!(got == want, "{what}: outcome under {p:?} differs from the reference kernels' ({got:?} vs {want:?}; None = panicked)");
}
want
}
/// Strides from ordinary to the largest a `usize` holds. 2^29 fits the `u32` stride
/// field of a WHM1 model; from 2^32 (64-bit targets) the product of the two strides
/// no longer fits in 64 bits.
///
/// 2^29 comes first, deliberately. A kernel that allocates one tap table per
/// (row, column) phase — as the fast transposed kernel did before it built tables
/// only for phases holding output — fails there at once (the table count exceeds
/// what any allocation can hold: "capacity overflow"). Placed after 2^16 it would
/// first commit memory for 2^32 tables (hundreds of GiB) and drive the machine into
/// swap before failing.
fn strides() -> Vec<usize> {
#[allow(unused_mut)] // pushed to on 64-bit targets only
let mut s = vec![1 << 29, 1, 2, 3, 1 << 13, 1 << 16, usize::MAX];
#[cfg(target_pointer_width = "64")]
s.push(1 << 32);
s
}
/// A one-layer int_hyper model.
fn hyper_model(layer: serde_json::Value) -> wai::int_hyper_synth::IntModel {
wai::int_hyper_synth::IntModel::from_json(&json!({ "afrac": 0, "gf": 0, "shift": 0, "layers": [layer] }))
}
/// An int_synth payload: a 1×1 output image from one int8 latent (C = 1, S = 1).
fn synth_payload(latent: i8) -> Vec<u8> {
let mut p = Vec::new();
p.extend(1u32.to_le_bytes()); // H
p.extend(1u32.to_le_bytes()); // W
p.extend(1u16.to_le_bytes()); // C
p.extend([0u8, 0]); // S_log2, reserved
p.push(latent as u8);
p
}
/// A one-layer int_synth spec: 1 → 3 channels, 3×3 kernel `1..=27` (channel `oc`
/// holds `9·oc + KERNEL`), shift 0, no ReLU.
fn synth_spec(stride: usize, pad: usize, out_pad: usize, bias: [i32; 3]) -> Vec<u8> {
let weight: Vec<i32> = (1..=27).collect();
json!({ "layers": [{
"in_ch": 1, "out_ch": 3, "k": 3, "stride": stride, "pad": pad, "out_pad": out_pad,
"weight": weight, "bias": bias, "shift": 0, "relu": false
}]})
.to_string()
.into_bytes()
}
#[test]
fn a_1x1_input_upsampled_by_any_stride() {
// k = 3, pad 1: the output is 1×1 and receives only the centre tap, 5·x.
for s in strides() {
let got = same_under_every_policy(&format!("JPEG-AI conv_transpose, stride {s}"), || {
wai::int_transformer::conv_transpose(&[5], 1, 1, 1, &KERNEL, 1, 3, s, 1, 0, &[1], &[0], 0)
});
assert_eq!(got, (vec![25], 1, 1), "JPEG-AI conv_transpose, stride {s}");
let model = hyper_model(json!({
"type": "convT", "out_ch": 1, "k": 3, "stride": s, "pad": 1, "opad": 0,
"weight": KERNEL, "mult": [1], "bias": [0]
}));
let got = same_under_every_policy(&format!("int_hyper convT, stride {s}"), || {
wai::int_hyper_synth::run_net(&model, &[5], 1, 1, 1)
});
assert_eq!(got, (vec![25], 1, 1, 1), "int_hyper convT, stride {s}");
let (payload, spec) = (synth_payload(5), synth_spec(s, 1, 0, [0, 0, 0]));
let got = same_under_every_policy(&format!("int_synth, stride {s}"), || {
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
});
if s <= u32::MAX as usize {
// centre taps 5, 14, 23 of the three output channels, times 5
assert_eq!(got, Ok(vec![25, 70, 115]), "int_synth, stride {s}");
} else {
// int_synth decodes through its lowering, whose `WHG1` fields are 32 bits: a
// stride past them is refused, a planned break.
assert!(got.is_err(), "int_synth, stride {s}: a stride past 32 bits is refused");
}
}
}
#[test]
fn a_regular_conv_whose_stride_exceeds_its_padded_input() {
// 5×5 input 1..=25, k = 3. Pad 1 (7×7 padded): from stride 5 on, one output,
// the taps that meet x[0], x[1], x[5], x[6]: 5·1 + 6·2 + 8·6 + 9·7. Pad 2 (9×9
// padded): from stride 7 on, one output, only tap (2, 2) meets x[0]: 9·1. At
// stride usize::MAX, pad 2 puts two leading column phases before the first input
// column, whose position must be found without overflowing.
let x: Vec<i64> = (1..=25).collect();
for (pad, want, first) in [(1usize, 5 + 6 * 2 + 8 * 6 + 9 * 7, 5usize), (2, 9, 7)] {
#[allow(unused_mut)] // pushed to on 64-bit targets only
let mut ss = vec![first, 7, 8, 9, 1 << 13, 1 << 16, 1 << 29, usize::MAX];
#[cfg(target_pointer_width = "64")]
ss.push(1 << 32);
for s in ss {
let got = same_under_every_policy(&format!("JPEG-AI conv_grouped, pad {pad}, stride {s}"), || {
wai::int_transformer::conv_grouped(&x, 1, 5, 5, &KERNEL, 1, 3, s, pad, 1, &[1], &[0], 0)
});
assert_eq!(got, (vec![want], 1, 1), "JPEG-AI conv_grouped, pad {pad}, stride {s}");
let model = hyper_model(json!({
"type": "conv", "out_ch": 1, "k": 3, "stride": s, "pad": pad,
"weight": KERNEL, "mult": [1], "bias": [0]
}));
let got = same_under_every_policy(&format!("int_hyper conv, pad {pad}, stride {s}"), || {
wai::int_hyper_synth::run_net(&model, &x, 1, 5, 5)
});
assert_eq!(got, (vec![want], 1, 1, 1), "int_hyper conv, pad {pad}, stride {s}");
}
}
}
#[test]
fn padding_far_beyond_the_kernel() {
let p = 1usize << 12;
// Transposed: pad 2^12 with output padding 2^13 − 2 restores a 1×1 output that
// no tap reaches (it sits 2^12 rows into the uncropped plane). int_synth decodes
// through its lowering, whose run-time plan caps the uncropped output a transposed
// convolution forms at 2^26 elements (integer-operators §5.3); this one is
// 3·8193·8193, so every policy refuses it before anything is allocated, a planned
// break. With pad 2^9 and output padding 2^10 − 2 the uncropped output, 3·1025·1025,
// is within the cap, and the decode gives the bias.
let (payload, spec) = (synth_payload(5), synth_spec(1, p, 2 * p - 2, [10, 20, 30]));
let got = same_under_every_policy("int_synth, pad 2^12", || {
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
});
assert!(got.is_err(), "int_synth, pad 2^12: the uncropped output is past the cap");
let q = 1usize << 9;
let (payload, spec) = (synth_payload(5), synth_spec(1, q, 2 * q - 2, [10, 20, 30]));
let got = same_under_every_policy("int_synth, pad 2^9", || {
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
});
assert_eq!(got, Ok(vec![10, 20, 30]));
// Regular: a 1×1 input, 1×1 kernel, pad 2^12, stride 2^13 — a 2×2 output whose
// samples all fall in the padding, so each is the bias.
let got = same_under_every_policy("JPEG-AI conv_grouped, pad 2^12", || {
wai::int_transformer::conv_grouped(&[5], 1, 1, 1, &[3], 1, 1, 2 * p, p, 1, &[1], &[7], 0)
});
assert_eq!(got, (vec![7; 4], 2, 2));
let model = hyper_model(json!({
"type": "conv", "out_ch": 1, "k": 1, "stride": 2 * p, "pad": p, "weight": [3], "mult": [1], "bias": [7]
}));
let got = same_under_every_policy("int_hyper conv, pad 2^12", || wai::int_hyper_synth::run_net(&model, &[5], 1, 1, 1));
assert_eq!(got, (vec![7; 4], 1, 2, 2));
}
/// An int_synth payload: an `h`×`w` output image from a 2×2 int8 latent (C = 1,
/// S = 2), latents in row-major order.
#[cfg(target_pointer_width = "64")]
fn synth_payload_2x2(h: u32, w: u32, latents: [i8; 4]) -> Vec<u8> {
let mut p = Vec::new();
p.extend(h.to_le_bytes());
p.extend(w.to_le_bytes());
p.extend(1u16.to_le_bytes()); // C
p.extend([1u8, 0]); // S_log2 = 1, reserved
p.extend(latents.map(|v| v as u8));
p
}
// Transposed convolutions of an input with two or more rows and columns, whose
// stride and padding make the reference's own size arithmetic overflow. The
// int_hyper, int_audio and JPEG-AI references size an uncropped plane
// `cout·(ho + 2·pad)·(wo + 2·pad)` in unchecked `usize` arithmetic. With overflow
// checks on, the reference panics on that arithmetic. With them off it wraps and then
// indexes out of the wrapped buffer. (int_synth decodes through its lowering, whose
// plan forms every size in checked 64-bit arithmetic and caps it: it refuses these,
// below.) A fast policy must have
// the same outcome: it may not return a value where the reference panics. A 1×1
// input cannot show this, since `(h − 1)·stride` is 0. The int_audio 1-D form is
// tested through its lowering's graph (`int_audio` unit tests).
/// A 3×3 input, and a 2×2 one with fields that fit a WHM1 model's `u32`.
const X: [i64; 9] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/// Stride 2^32 − 1 and pad 2^31 − 1: a 2×2 output from a 2×2 input, k = 1 (or a
/// 4×4 one, k = 3), under an uncropped plane of 2^32 × 2^32. On a 64-bit target
/// the plane size overflows; on a 32-bit one the stride arithmetic already does.
const S32: usize = u32::MAX as usize;
const P32: usize = (u32::MAX >> 1) as usize;
#[test]
fn jpegai_conv_transpose_where_the_reference_plane_size_overflows() {
// 3×3 input, k = 1, stride = pad = 2^62: a 1×1 output, but an uncropped plane
// of (2^63 + 1)² elements. The reference panics in both profiles.
#[cfg(target_pointer_width = "64")]
{
let s = 1usize << 62;
let got = same_outcome_under_every_policy("JPEG-AI conv_transpose, 3×3, stride = pad = 2^62", || {
wai::int_transformer::conv_transpose(&X, 1, 3, 3, &[7], 1, 1, s, s, 0, &[1], &[0], 0)
});
assert_eq!(got, None, "the reference panics");
}
let got = same_outcome_under_every_policy("JPEG-AI conv_transpose, 2×2, u32 stride and pad", || {
wai::int_transformer::conv_transpose(&X[..4], 1, 2, 2, &[7], 1, 1, S32, P32, 0, &[1], &[0], 0)
});
assert_eq!(got, None, "u32 fields: the reference panics");
}
#[test]
fn int_hyper_convt_where_the_reference_plane_size_overflows() {
// As above, through the int_hyper decoder.
let convt = |s: usize, p: usize| {
hyper_model(json!({
"type": "convT", "out_ch": 1, "k": 1, "stride": s, "pad": p, "opad": 0,
"weight": [7], "mult": [1], "bias": [0]
}))
};
#[cfg(target_pointer_width = "64")]
{
let model = convt(1 << 62, 1 << 62);
let got = same_outcome_under_every_policy("int_hyper convT, 3×3, stride = pad = 2^62", || {
wai::int_hyper_synth::run_net(&model, &X, 1, 3, 3)
});
assert_eq!(got, None, "the reference panics");
}
let model = convt(S32, P32);
let got = same_outcome_under_every_policy("int_hyper convT, 2×2, u32 stride and pad", || {
wai::int_hyper_synth::run_net(&model, &X[..4], 1, 2, 2)
});
assert_eq!(got, None, "u32 fields: the reference panics");
}
#[cfg(target_pointer_width = "64")]
#[test]
fn int_synth_where_the_reference_output_size_overflows_i64() {
// 2×2 latent, k = 3, stride = pad = out_pad = 2^62: a 3×3 output. The direct
// decode computed (2 − 1)·2^62 + 3 + 2^62 − 2·2^62 in `i64`, which overflows
// (2^63 + 3) although every `usize` step fits: it panicked with overflow checks on
// and, with them off, returned the wrapped size's output. int_synth decodes through
// its lowering (integer-operators §13.1), whose `WHG1` fields are 32 bits: in every
// build and under every policy the decode is refused, not panicked. A planned break.
let s = 1usize << 62;
let (payload, spec) = (synth_payload_2x2(3, 3, [1, 2, 3, 4]), synth_spec(s, s, s, [0, 0, 0]));
let got = same_outcome_under_every_policy("int_synth, 2×2 latent, stride = pad = out_pad = 2^62", || {
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
});
assert!(matches!(got, Some(Err(_))), "refused, not panicked: {got:?}");
// The u32 fields fit `WHG1`, but the uncropped side (2 − 1)·(2^32 − 1) + 3 is past
// the run-time plan's cap of 2^26: refused before anything is allocated (the direct
// decode returned the bias), a planned break.
let (payload, spec) = (synth_payload_2x2(4, 4, [1, 2, 3, 4]), synth_spec(S32, P32, 0, [10, 20, 30]));
let got = same_outcome_under_every_policy("int_synth, 2×2 latent, u32 stride and pad", || {
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
});
assert!(matches!(got, Some(Err(_))), "u32 fields: refused by the cap: {got:?}");
}