From 8356268251fc3f8390aa93ccb20d2f27df438287 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:30:39 -0400 Subject: [PATCH 01/22] wai-ops: the bounds pass in closed form for conv, conv_transpose and igdn (WIP, committed before the first build) Each convolution's term hull and partial-sum interval come from four sums per output channel (least and greatest weight, sum of the positive and of the negative weights; per residue for conv_transpose), and each igdn row folds in i128, wherever no operation of the rule's fold can saturate; elsewhere the rule folds tap by tap as before. The pass before this change is kept, test-only (feature `oracle` for other crates), as bounds_oracle, with a differential over the committed graphs, the pretrained set when present, and random and adversarial operators and graphs. Co-Authored-By: Claude Opus 5.5 --- wai/wai-ops/Cargo.toml | 6 + wai/wai-ops/src/bounds.rs | 282 +++++++- wai/wai-ops/src/bounds_oracle.rs | 1083 ++++++++++++++++++++++++++++++ wai/wai-ops/src/lib.rs | 4 + 4 files changed, 1342 insertions(+), 33 deletions(-) create mode 100644 wai/wai-ops/src/bounds_oracle.rs diff --git a/wai/wai-ops/Cargo.toml b/wai/wai-ops/Cargo.toml index a96d72394..85519fafc 100644 --- a/wai/wai-ops/Cargo.toml +++ b/wai/wai-ops/Cargo.toml @@ -28,5 +28,11 @@ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] } [dependencies] # none +[features] +# `bounds_oracle`: the bounds pass as it stood before its closed forms, for another +# crate's tests to hold `bounds::pass` to (wai-rs's random-set differential). This +# crate's own tests see it without the feature. Never on a decode's path. +oracle = [] + [profile.release] debug = false diff --git a/wai/wai-ops/src/bounds.rs b/wai/wai-ops/src/bounds.rs index 010e7a2c2..e68028498 100644 --- a/wai/wai-ops/src/bounds.rs +++ b/wai/wai-ops/src/bounds.rs @@ -233,6 +233,7 @@ pub struct Inter { } /// One operator's rules applied to the intervals of its inputs. +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Eval { pub(crate) inter: Vec, pub(crate) results: Vec, @@ -275,6 +276,212 @@ fn taps(ar: &mut Ar, ws: &[i64], x: Iv, b: i64) -> (Iv, Iv) { (th.unwrap_or(Iv::point(Z)), Iv::new(lo, hi)) } +// ------------------------------------------------------------------ closed forms +// +// A `conv` or `conv_transpose` reads millions of weights in a large set, and an `igdn` +// `C²` coefficients. Folding each term in 512 bits, as §8.2 and §8.3 write the rules, +// is most of a load's cost. The forms below compute the same integers from a few sums +// per output channel, with every operation exact. Each states the condition under which +// no operation of the rule's fold saturates (§8.1); there, the fold's result is the +// exact value, and so is the closed form's. Where the condition fails, the caller folds +// tap by tap as before, through `Ar`, so that saturation, and the node's `saturated` +// mark, come out as the rule makes them. The pass before these forms is kept, test-only, +// in `bounds_oracle`, and the two are compared on every committed set and on random +// and adversarial ones. + +/// An input interval `[l, h]` a closed form reads: both ends in `i128` and `l ≤ h`. A +/// weight lies in `i64`, so a term `w·l` then lies within `2^63 · 2^127 = 2^190`, and no +/// product the rule forms saturates. +fn narrow(x: Iv) -> Option<(i128, i128)> { + match (x.lo.to_i128(), x.hi.to_i128()) { + (Some(l), Some(h)) if l <= h => Some((l, h)), + _ => None, + } +} + +/// The sums of a tap set's positive weights and of its negative weights. A tap set holds +/// fewer than `2^64` weights, each of magnitude at most `2^63`, so each sum lies strictly +/// inside `±(2^127 − 2^63)`, and `i128` holds it. +#[derive(Clone, Copy, Default)] +struct Sums { + pos: i128, + neg: i128, +} + +impl Sums { + #[inline] + fn add(&mut self, w: i64) { + self.pos += w.max(0) as i128; + self.neg += w.min(0) as i128; + } +} + +/// `A(K, b)` (§8.2) of a tap set `K` whose weights sum to `s`, over `[l, h]` with +/// `l ≤ h`, in closed form; `None` when the rule's fold saturates. +/// +/// A weight `w > 0` has the term `[w·l, w·h]`, so `min(0, w·l) = w·min(0, l)` and +/// `max(0, w·h) = w·max(0, h)`. A weight `w < 0` has the term `[w·h, w·l]`, so +/// `min(0, w·h) = w·max(0, h)` and `max(0, w·l) = w·min(0, l)`. A weight 0 adds 0. So +/// `lo = min(0, b) + min(0, l)·S⁺ + max(0, h)·S⁻` and +/// `hi = max(0, b) + max(0, h)·S⁺ + min(0, l)·S⁻`, each formed exactly: a product lies +/// within `2^127 · 2^127`, a sum within `2^256`. +/// +/// The rule's fold for `lo` starts at `min(0, b) ≤ 0` and adds terms `≤ 0`, so its partial +/// sums fall from there to the final sum (and `hi`'s rise). When the final sum lies in +/// `[−2^255, 2^255]`, every partial sum does, no addition saturates, and the fold's result +/// is the exact sum. When it does not, the fold saturates on the way. +fn acc_closed(s: Sums, l: i128, h: i128, b: i64) -> Option { + let (l0, h0) = (I512::from_i128(l.min(0)), I512::from_i128(h.max(0))); + let (p, n) = (I512::from_i128(s.pos), I512::from_i128(s.neg)); + let lo = i(b.min(0)).add(l0.mul(p)).add(h0.mul(n)); + let hi = i(b.max(0)).add(h0.mul(p)).add(l0.mul(n)); + let cap = I512::pow2(255); + (lo >= cap.neg() && hi <= cap).then_some(Iv::new(lo, hi)) +} + +/// The hull of the term intervals (§8.2) of weights whose least is `wmin` and greatest +/// `wmax`, over `[l, h]`. For a fixed end `x`, `w ↦ w·x` is monotone, so its least and +/// greatest values over the weights are at `wmin` or `wmax`: the hull is the corner hull +/// of `[wmin, wmax]` and `[l, h]`. Exact: every product lies within `2^190`. +fn term_closed(wmin: i64, wmax: i64, l: i128, h: i128) -> Iv { + let (a, b) = (i(wmin), i(wmax)); + let (l, h) = (I512::from_i128(l), I512::from_i128(h)); + let c = [a.mul(l), a.mul(h), b.mul(l), b.mul(h)]; + Iv::new(c.iter().copied().fold(c[0], I512::min), c.iter().copied().fold(c[0], I512::max)) +} + +/// [`taps`] in closed form, for a tap set `ws` over `[l, h]` (see [`narrow`]): the term +/// hull and `A(K, b)`. `None` for an empty tap set, or where the fold saturates. +fn taps_closed(ws: &[i64], l: i128, h: i128, b: i64) -> Option<(Iv, Iv)> { + if ws.is_empty() { + return None; + } + let (mut mn, mut mx, mut s) = (i64::MAX, i64::MIN, Sums::default()); + for &w in ws { + mn = mn.min(w); + mx = mx.max(w); + s.add(w); + } + Some((term_closed(mn, mx, l, h), acc_closed(s, l, h, b)?)) +} + +/// What `conv_transpose`'s closed form reads, from one pass over its weights in their +/// order (`[cin][cout][kh][kw]`): each output channel's least and greatest weight, and +/// the sums of each output channel's taps of each residue (`nb` residues per channel). +struct ConvTSums { + min: Vec, + max: Vec, + sums: Vec, +} + +/// [`ConvTSums`] of weights `w` for `cin` input and `cout` output channels; `None` when +/// the weights are not `cin · cout · kh · kw` (only a graph built by hand can be so) or a +/// stride is 0, where the caller folds tap by tap as before. +#[allow(clippy::too_many_arguments)] +fn convt_sums(w: &[i64], cin: usize, cout: usize, kh: usize, kw: usize, sh: usize, sw: usize, rw: usize, nb: usize) -> Option { + let kk = kh.checked_mul(kw)?; + if kk == 0 || cout == 0 || sh == 0 || sw == 0 || cin.checked_mul(cout)?.checked_mul(kk)? != w.len() { + return None; + } + // Tap (ii, jj) of a kernel is its residue (ii mod sh, jj mod sw)'s. + let residue: Vec = (0..kk).map(|e| ((e / kw) % sh) * rw + (e % kw) % sw).collect(); + let (mut min, mut max) = (vec![i64::MAX; cout], vec![i64::MIN; cout]); + let mut sums = vec![Sums::default(); cout * nb]; + for (k, blk) in w.chunks_exact(kk).enumerate() { + let o = k % cout; + let (mut mn, mut mx) = (min[o], max[o]); + let so = &mut sums[o * nb..(o + 1) * nb]; + for (&v, &r) in blk.iter().zip(&residue) { + mn = mn.min(v); + mx = mx.max(v); + so[r].add(v); + } + (min[o], max[o]) = (mn, mx); + } + Some(ConvTSums { min, max, sums }) +} + +/// `conv_transpose`'s output channel `o` tap by tap, as §8.3 writes the rule: the hull of +/// its taps' term intervals (`None` for no taps), and the hull over residues of +/// `A(K_ρ, b)`. +#[allow(clippy::too_many_arguments)] +fn convt_taps(ar: &mut Ar, w: &[i64], x: Iv, cin: usize, cout: usize, o: usize, kh: usize, kw: usize, sh: usize, sw: usize, rw: usize, nb: usize, b: i64) -> (Option, Iv) { + let b = i(b); + let mut th: Option = None; + let mut buckets = vec![(b.min(Z), b.max(Z)); nb]; + for ci in 0..cin { + for ii in 0..kh { + for jj in 0..kw { + let wv = w[((ci * cout + o) * kh + ii) * kw + jj]; + let t = ar.term(i(wv), x); + th = hull_opt(th, t); + let k = (ii % sh) * rw + (jj % sw); + let (lo, hi) = buckets[k]; + buckets[k] = (ar.add(lo, t.lo.min(Z)), ar.add(hi, t.hi.max(Z))); + } + } + } + let mut a: Option = None; + for &(lo, hi) in &buckets { + a = hull_opt(a, Iv::new(lo, hi)); + } + (th, a.unwrap()) +} + +/// `⌊v / 2^k⌋` for `|v| < 2^127`: an arithmetic shift, `−1` or `0` once `k ≥ 127`. +fn shr128(v: i128, k: u32) -> i128 { + if k >= 127 { if v < 0 { -1 } else { 0 } } else { v >> k } +} + +/// One output channel of `igdn`'s sums over `j` (§8.3), in `i128`: the hulls of its +/// `gx2_ij` and `term_ij`, `igdn.N`'s interval, and the final norm's two ends. +struct IgdnRow { + gx: (i128, i128), + term: (i128, i128), + n: (i128, i128), + f: (i128, i128), +} + +/// [`IgdnRow`] of the coefficients `gam` (`γ[i][·]`) and `β_i = beta`, over +/// `igdn.x2 = [x2lo, x2hi]` with shift `gf`; `None` for an empty row, or where a product +/// or a partial sum leaves `i128`, when the caller forms the row in 512 bits as the rule +/// writes it. Where every value stays in `i128` it lies within `2^127 < 2^255`, so no +/// operation of the rule saturates, and these are the rule's values: the left folds over +/// `j` add exactly, in any order. +fn igdn_row(gam: &[i64], x2lo: i128, x2hi: i128, gf: u32, beta: i64) -> Option { + if gam.is_empty() { + return None; + } + let b = beta as i128; + let (mut plo, mut phi, mut flo, mut fhi) = (b.min(0), b.max(0), b, b); + let (mut gxl, mut gxh, mut tl, mut thi) = (i128::MAX, i128::MIN, i128::MAX, i128::MIN); + for &g in gam { + let (a, c) = ((g as i128).checked_mul(x2lo)?, (g as i128).checked_mul(x2hi)?); + let (glo, ghi) = (a.min(c), a.max(c)); + gxl = gxl.min(glo); + gxh = gxh.max(ghi); + let (t0, t1) = (shr128(glo, gf), shr128(ghi, gf)); + tl = tl.min(t0); + thi = thi.max(t1); + plo = plo.checked_add(t0.min(0))?; + phi = phi.checked_add(t1.max(0))?; + flo = flo.checked_add(t0)?; + fhi = fhi.checked_add(t1)?; + } + Some(IgdnRow { gx: (gxl, gxh), term: (tl, thi), n: (plo, phi), f: (flo, fhi) }) +} + +fn iv128(v: (i128, i128)) -> Iv { + Iv::new(I512::from_i128(v.0), I512::from_i128(v.1)) +} + +fn hull2(a: Option, b: Option) -> Option { + match b { + None => a, + Some(b) => hull_opt(a, b), + } +} + /// The `exp_neg_q16` rule (§8.3), given the interval of `u = −x`. fn exp_rule(ar: &mut Ar, inter: &mut Vec, uv: Iv) { let k = u(45426); @@ -308,10 +515,13 @@ pub(crate) fn eval(g: &Graph, op: &Op, ins: &[Iv], sizes: Option<&[(u64, u64)]>) (Params::Conv { cout, kh, kw, groups, rq, w, .. }, _) => { let (cout, cg) = (*cout as usize, cin / *groups as usize); let n = cg * *kh as usize * *kw as usize; + let fast = narrow(x); let (mut th, mut ah) = (None, None); let mut ts = Vec::with_capacity(cout); for o in 0..cout { - let (t, a) = taps(&mut ar, &w[o * n..(o + 1) * n], x, rq.pre(o)); + let ws = &w[o * n..(o + 1) * n]; + let closed = fast.and_then(|(l, h)| taps_closed(ws, l, h, rq.pre(o))); + let (t, a) = closed.unwrap_or_else(|| taps(&mut ar, ws, x, rq.pre(o))); th = hull_opt(th, t); ah = hull_opt(ah, a); ts.push(a); @@ -325,28 +535,21 @@ pub(crate) fn eval(g: &Graph, op: &Op, ins: &[Iv], sizes: Option<&[(u64, u64)]>) // Residues with no tap give A(∅, b), which every other residue's interval // holds; the hull is over the residues that have taps. let (rh, rw) = (sh.min(kh), sw.min(kw)); + let nb = rh * rw; + let fast = narrow(x).and_then(|lh| Some((lh, convt_sums(w, cin, cout, kh, kw, sh, sw, rw, nb)?))); let (mut th, mut ah) = (None, None); let mut ts = Vec::with_capacity(cout); for o in 0..cout { - let b = i(rq.pre(o)); - let mut buckets = vec![(b.min(Z), b.max(Z)); rh * rw]; - for ci in 0..cin { - for ii in 0..kh { - for jj in 0..kw { - let wv = w[((ci * cout + o) * kh + ii) * kw + jj]; - let t = ar.term(i(wv), x); - th = hull_opt(th, t); - let k = (ii % sh) * rw + (jj % sw); - let (lo, hi) = buckets[k]; - buckets[k] = (ar.add(lo, t.lo.min(Z)), ar.add(hi, t.hi.max(Z))); - } + let b = rq.pre(o); + let closed = fast.as_ref().and_then(|&((l, h), ref st)| { + let mut a: Option = None; + for &s in &st.sums[o * nb..(o + 1) * nb] { + a = hull_opt(a, acc_closed(s, l, h, b)?); } - } - let mut a: Option = None; - for &(lo, hi) in &buckets { - a = hull_opt(a, Iv::new(lo, hi)); - } - let a = a.unwrap(); + Some((Some(term_closed(st.min[o], st.max[o], l, h)), a?)) + }); + let (t, a) = closed.unwrap_or_else(|| convt_taps(&mut ar, w, x, cin, cout, o, kh, kw, sh, sw, rw, nb, b)); + th = hull2(th, t); ah = hull_opt(ah, a); ts.push(a); } @@ -442,22 +645,35 @@ pub(crate) fn eval(g: &Graph, op: &Op, ins: &[Iv], sizes: Option<&[(u64, u64)]>) let (l2, h2) = (ar.mul(x.lo, x.lo), ar.mul(x.hi, x.hi)); let x2 = if x.lo <= Z && Z <= x.hi { Iv::new(Z, l2.max(h2)) } else { Iv::new(l2.min(h2), l2.max(h2)) }; let c = cin; + let x2n = narrow(x2); let (mut gx, mut tm, mut nn, mut rr, mut xr, mut res) = (None, None, None, None, None, None); for oi in 0..c { - let b = i(beta[oi]); - let (mut plo, mut phi) = (b.min(Z), b.max(Z)); - let (mut flo, mut fhi) = (b, b); - for j in 0..c { - let gv = i(gamma[oi * c + j]); - let g2 = ar.corner(Iv::point(gv), x2); - gx = hull_opt(gx, g2); - let t = Iv::new(ar.shr(g2.lo, *gf), ar.shr(g2.hi, *gf)); - tm = hull_opt(tm, t); - plo = ar.add(plo, t.lo.min(Z)); - phi = ar.add(phi, t.hi.max(Z)); - flo = ar.add(flo, t.lo); - fhi = ar.add(fhi, t.hi); - } + let row = x2n.and_then(|(a, b)| igdn_row(&gamma[oi * c..(oi + 1) * c], a, b, *gf, beta[oi])); + let (plo, phi, flo, fhi) = match row { + Some(r) => { + gx = hull_opt(gx, iv128(r.gx)); + tm = hull_opt(tm, iv128(r.term)); + let (n, f) = (iv128(r.n), iv128(r.f)); + (n.lo, n.hi, f.lo, f.hi) + } + None => { + let b = i(beta[oi]); + let (mut plo, mut phi) = (b.min(Z), b.max(Z)); + let (mut flo, mut fhi) = (b, b); + for j in 0..c { + let gv = i(gamma[oi * c + j]); + let g2 = ar.corner(Iv::point(gv), x2); + gx = hull_opt(gx, g2); + let t = Iv::new(ar.shr(g2.lo, *gf), ar.shr(g2.hi, *gf)); + tm = hull_opt(tm, t); + plo = ar.add(plo, t.lo.min(Z)); + phi = ar.add(phi, t.hi.max(Z)); + flo = ar.add(flo, t.lo); + fhi = ar.add(fhi, t.hi); + } + (plo, phi, flo, fhi) + } + }; nn = hull_opt(nn, Iv::new(plo, phi)); let (a, b2) = (ar.max(Z, flo), ar.max(Z, fhi)); let r = Iv::new(ar.isqrt(a), ar.isqrt(b2)); diff --git a/wai/wai-ops/src/bounds_oracle.rs b/wai/wai-ops/src/bounds_oracle.rs new file mode 100644 index 000000000..42ba333b7 --- /dev/null +++ b/wai/wai-ops/src/bounds_oracle.rs @@ -0,0 +1,1083 @@ +//! The bounds pass as it stood before its closed forms (wai-ops at `4d7b9e28`): every +//! tap of a `conv` or `conv_transpose`, and every `(i, j)` of an `igdn`, folded one by +//! one in 512 bits through [`Ar`], exactly as §8.1–§8.3 write the rules. Test-only +//! (`cfg(test)`, or the `oracle` feature for another crate's tests): it is the oracle +//! the closed forms of [`crate::bounds`] are held to, and it is never on a decode's +//! path. Its result is the same [`Bounds`], so the two compare whole: every node's +//! intermediates, producer results, saturation, parameter conditions and flags, and +//! every tensor's interval; and the same refusals. + +use alloc::vec; +use alloc::vec::Vec; + +use crate::bounds::{Bounds, Class, Inter, Iv, Node}; +use crate::code::{Code, Refusal, Site}; +use crate::graph::{BiasKind, Graph, Op, Params, Policy, Round, Rq, opcode::*}; +use crate::xint::I512; + +fn hull_opt(a: Option, b: Iv) -> Option { + Some(match a { + None => b, + Some(a) => a.hull(b), + }) +} + +const Z: I512 = I512::ZERO; + +fn i(v: i64) -> I512 { + I512::from_i64(v) +} +fn u(v: u64) -> I512 { + I512::from_u64(v) +} + +/// The arithmetic of the pass (§8.1): every primitive result is saturated at +/// `±2^255`, and `hit` records whether that changed a value. +#[derive(Default)] +struct Ar { + hit: bool, +} + +impl Ar { + fn s(&mut self, v: I512) -> I512 { + let (r, changed) = v.sat(); + self.hit |= changed; + r + } + fn add(&mut self, a: I512, b: I512) -> I512 { + self.s(a.add(b)) + } + fn sub(&mut self, a: I512, b: I512) -> I512 { + self.s(a.sub(b)) + } + fn mul(&mut self, a: I512, b: I512) -> I512 { + self.s(a.mul(b)) + } + fn neg(&mut self, a: I512) -> I512 { + self.s(a.neg()) + } + fn abs(&mut self, a: I512) -> I512 { + self.s(a.abs()) + } + fn min(&mut self, a: I512, b: I512) -> I512 { + self.s(a.min(b)) + } + fn max(&mut self, a: I512, b: I512) -> I512 { + self.s(a.max(b)) + } + /// `⌊a / b⌋`, `b > 0`. + fn fdiv(&mut self, a: I512, b: I512) -> I512 { + self.s(a.div_floor(b)) + } + /// `⌊a / 2^k⌋`. + fn shr(&mut self, a: I512, k: u32) -> I512 { + self.s(a.shr_floor(k)) + } + fn isqrt(&mut self, a: I512) -> I512 { + self.s(a.isqrt()) + } + fn isqrt_ceil(&mut self, a: I512) -> I512 { + self.s(a.isqrt_ceil()) + } + fn clamp(&mut self, v: I512, lo: I512, hi: I512) -> I512 { + let m = self.max(v, lo); + self.min(m, hi) + } + + /// The term interval of a weight `w` over `[l, h]` (§8.2). + fn term(&mut self, w: I512, x: Iv) -> Iv { + let (a, b) = (self.mul(w, x.lo), self.mul(w, x.hi)); + Iv::new(a.min(b), a.max(b)) + } + /// The corner hull of two run-time intervals (§8.2). + fn corner(&mut self, a: Iv, b: Iv) -> Iv { + let c = [self.mul(a.lo, b.lo), self.mul(a.lo, b.hi), self.mul(a.hi, b.lo), self.mul(a.hi, b.hi)]; + let lo = c.iter().copied().fold(c[0], I512::min); + let hi = c.iter().copied().fold(c[0], I512::max); + Iv::new(lo, hi) + } + /// `R_round(p, s)` (§2), evaluated as §2 writes it. + fn round(&mut self, p: I512, s: u32, r: Round) -> I512 { + if s == 0 { + return p; + } + match r { + Round::Floor => self.shr(p, s), + Round::HalfUp => { + let t = self.add(p, I512::pow2(s - 1)); + self.shr(t, s) + } + Round::HalfEven => { + let q = self.shr(p, s); + let qs = self.mul(q, I512::pow2(s)); + let rem = self.sub(p, qs); + let h = I512::pow2(s - 1); + if rem > h || (rem == h && q.is_odd()) { self.add(q, I512::ONE) } else { q } + } + } + } + /// The `rq` rules for one channel (§8.3): `rq.p`, `rq.q` and `rq.y` from `rq.t`. + fn rq_chan(&mut self, t: Iv, m: Option, s: u32, r: Round, post: i64) -> (Iv, Iv, Iv) { + let m = i(m.unwrap_or(1)); + let (a, b) = (self.mul(t.lo, m), self.mul(t.hi, m)); + let p = Iv::new(a.min(b), a.max(b)); + let add = if r != Round::Floor && s >= 1 { I512::pow2(s - 1) } else { Z }; + let rp = Iv::new(p.lo, self.add(p.hi, add)); + let q = Iv::new(self.round(p.lo, s, r), self.round(p.hi, s, r)); + let y = Iv::new(self.add(q.lo, i(post)), self.add(q.hi, i(post))); + (rp, q, y) + } +} + +/// One operator's rules applied to the intervals of its inputs, as the pass then +/// formed them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Eval { + pub inter: Vec, + pub results: Vec, + pub saturated: bool, + /// `lut16`'s input range passes `[−32767, 32767]`: a load refusal (§5.6). + pub lut_range: bool, +} + +/// The `rq` intermediates and producer result of a whole requantisation, given each +/// channel's `rq.t`; the per-channel rules are hulled (§8.1). +fn rq_all(ar: &mut Ar, inter: &mut Vec, ts: &[Iv], rq: &Rq) -> Iv { + let (mut t, mut p, mut q, mut y) = (None, None, None, None); + for (o, &to) in ts.iter().enumerate() { + let (po, qo, yo) = ar.rq_chan(to, rq.mult(o), rq.shift(o), rq.round, rq.post(o)); + t = hull_opt(t, to); + p = hull_opt(p, po); + q = hull_opt(q, qo); + y = hull_opt(y, yo); + } + let (t, p, q, y) = (t.unwrap(), p.unwrap(), q.unwrap(), y.unwrap()); + inter.push(Inter { key: "rq.t", class: Class::Acc, iv: t }); + inter.push(Inter { key: "rq.p", class: Class::Product, iv: p }); + inter.push(Inter { key: "rq.q", class: Class::Acc, iv: q }); + inter.push(Inter { key: "rq.y", class: Class::Acc, iv: y }); + y +} + +/// The term hull and the partial-sum interval `A(K, b)` (§8.2) of the weights `ws` +/// over the input interval `x`. +fn taps(ar: &mut Ar, ws: &[i64], x: Iv, b: i64) -> (Iv, Iv) { + let bb = i(b); + let (mut lo, mut hi) = (bb.min(Z), bb.max(Z)); + let mut th: Option = None; + for &w in ws { + let t = ar.term(i(w), x); + th = hull_opt(th, t); + lo = ar.add(lo, t.lo.min(Z)); + hi = ar.add(hi, t.hi.max(Z)); + } + (th.unwrap_or(Iv::point(Z)), Iv::new(lo, hi)) +} + +/// The `exp_neg_q16` rule (§8.3), given the interval of `u = −x`. +fn exp_rule(ar: &mut Ar, inter: &mut Vec, uv: Iv) { + let k = u(45426); + let (nlo, nhi) = (ar.fdiv(uv.lo, k), ar.fdiv(uv.hi, k)); + let nl = Iv::new(ar.mul(k, nlo), ar.mul(k, nhi)); + let t2 = Iv::new(ar.mul(u(43245), u(43245)), ar.mul(u(88670), u(88670))); + let sq = ar.mul(u(88670), u(88670)); + let q = ar.shr(sq, 16); + let c = Iv::new(Z, ar.mul(u(23495), q)); + inter.push(Inter { key: "exp.negx", class: Class::Acc, iv: uv }); + inter.push(Inter { key: "exp.nl", class: Class::Acc, iv: nl }); + inter.push(Inter { key: "exp.t2", class: Class::Acc, iv: t2 }); + inter.push(Inter { key: "exp.c", class: Class::Acc, iv: c }); +} + +/// The size of a reduction (§8.1, *Sizes*): `H·W` or `W` of tensor `t`, planned on the +/// declared sizes. +fn size_of(sizes: Option<&[(u64, u64)]>, t: u32) -> (u64, u64) { + sizes.map(|s| s[t as usize]).unwrap_or((0, 0)) +} + +/// Apply operator `op`'s rules (§8.3) to `ins`, the intervals of its inputs in input +/// order. +pub fn eval(g: &Graph, op: &Op, ins: &[Iv], sizes: Option<&[(u64, u64)]>) -> Eval { + let mut ar = Ar::default(); + let mut inter: Vec = Vec::new(); + let x = ins[0]; + let cin = g.tensors[op.ins[0] as usize].channels as usize; + let mut lut_range = false; + let results: Vec = match (&op.p, op.code) { + (Params::Conv { cout, kh, kw, groups, rq, w, .. }, _) => { + let (cout, cg) = (*cout as usize, cin / *groups as usize); + let n = cg * *kh as usize * *kw as usize; + let (mut th, mut ah) = (None, None); + let mut ts = Vec::with_capacity(cout); + for o in 0..cout { + let (t, a) = taps(&mut ar, &w[o * n..(o + 1) * n], x, rq.pre(o)); + th = hull_opt(th, t); + ah = hull_opt(ah, a); + ts.push(a); + } + inter.push(Inter { key: "conv.term", class: Class::Acc, iv: th.unwrap() }); + inter.push(Inter { key: "conv.acc", class: Class::Acc, iv: ah.unwrap() }); + vec![rq_all(&mut ar, &mut inter, &ts, rq)] + } + (Params::ConvT { cout, kh, kw, sh, sw, rq, w, .. }, _) => { + let (cout, kh, kw, sh, sw) = (*cout as usize, *kh as usize, *kw as usize, *sh as usize, *sw as usize); + // Residues with no tap give A(∅, b), which every other residue's interval + // holds; the hull is over the residues that have taps. + let (rh, rw) = (sh.min(kh), sw.min(kw)); + let (mut th, mut ah) = (None, None); + let mut ts = Vec::with_capacity(cout); + for o in 0..cout { + let b = i(rq.pre(o)); + let mut buckets = vec![(b.min(Z), b.max(Z)); rh * rw]; + for ci in 0..cin { + for ii in 0..kh { + for jj in 0..kw { + let wv = w[((ci * cout + o) * kh + ii) * kw + jj]; + let t = ar.term(i(wv), x); + th = hull_opt(th, t); + let k = (ii % sh) * rw + (jj % sw); + let (lo, hi) = buckets[k]; + buckets[k] = (ar.add(lo, t.lo.min(Z)), ar.add(hi, t.hi.max(Z))); + } + } + } + let mut a: Option = None; + for &(lo, hi) in &buckets { + a = hull_opt(a, Iv::new(lo, hi)); + } + let a = a.unwrap(); + ah = hull_opt(ah, a); + ts.push(a); + } + inter.push(Inter { key: "convT.term", class: Class::Acc, iv: th.unwrap() }); + inter.push(Inter { key: "convT.acc", class: Class::Acc, iv: ah.unwrap() }); + vec![rq_all(&mut ar, &mut inter, &ts, rq)] + } + (Params::Rescale(rq), _) => { + let ts: Vec = (0..cin).map(|c| Iv::new(ar.add(x.lo, i(rq.pre(c))), ar.add(x.hi, i(rq.pre(c))))).collect(); + vec![rq_all(&mut ar, &mut inter, &ts, rq)] + } + (Params::Chscale { scalar, round, shift, m, .. }, _) => { + let s = g.scalars[*scalar as usize]; + let (mut p, mut q) = (None, None); + for c in 0..cin { + for row in s.lo..=s.hi { + let (po, qo, _) = ar.rq_chan(x, Some(m[row as usize * cin + c]), *shift as u32, *round, 0); + p = hull_opt(p, po); + q = hull_opt(q, qo); + } + } + let (p, q) = (p.unwrap(), q.unwrap()); + inter.push(Inter { key: "rq.t", class: Class::Acc, iv: x }); + inter.push(Inter { key: "rq.p", class: Class::Product, iv: p }); + inter.push(Inter { key: "rq.q", class: Class::Acc, iv: q }); + inter.push(Inter { key: "rq.y", class: Class::Acc, iv: q }); + vec![q] + } + (Params::Mul(rq), _) => { + let mp = ar.corner(x, ins[1]); + inter.push(Inter { key: "mul.p", class: Class::Acc, iv: mp }); + let ts = vec![mp; cin]; + vec![rq_all(&mut ar, &mut inter, &ts, rq)] + } + (Params::Gram { rq, .. }, _) => { + let t = ar.corner(x, ins[1]); + let (h0, w0) = size_of(sizes, op.ins[0]); + let (h1, w1) = size_of(sizes, op.ins[1]); + let l = u((h0 * w0).max(h1 * w1)); + let acc = Iv::new(ar.mul(l, t.lo.min(Z)), ar.mul(l, t.hi.max(Z))); + inter.push(Inter { key: "gram.term", class: Class::Acc, iv: t }); + inter.push(Inter { key: "gram.acc", class: Class::Acc, iv: acc }); + let ts = vec![acc; cin]; + vec![rq_all(&mut ar, &mut inter, &ts, rq)] + } + (Params::Apply { groups, rq }, _) => { + let t = ar.corner(x, ins[1]); + let d = u((cin / *groups as usize) as u64); + let acc = Iv::new(ar.mul(d, t.lo.min(Z)), ar.mul(d, t.hi.max(Z))); + inter.push(Inter { key: "apply.term", class: Class::Acc, iv: t }); + inter.push(Inter { key: "apply.acc", class: Class::Acc, iv: acc }); + let ts = vec![acc; cin]; + vec![rq_all(&mut ar, &mut inter, &ts, rq)] + } + (_, ADD) => vec![Iv::new(ar.add(x.lo, ins[1].lo), ar.add(x.hi, ins[1].hi))], + (_, CHUNK_ADD) => vec![Iv::new(ar.mul(u(2), x.lo), ar.mul(u(2), x.hi))], + (Params::Clamp { lo, hi }, _) => vec![Iv::new(ar.clamp(x.lo, i(*lo), i(*hi)), ar.clamp(x.hi, i(*lo), i(*hi)))], + (_, RELU) => vec![Iv::new(ar.max(x.lo, Z), ar.max(x.hi, Z))], + (_, ABS) => vec![abs_image(&mut ar, x)], + (Params::Leaky { a, f }, _) => { + let mut r: Option = None; + if x.lo.is_neg() { + let hn = ar.min(x.hi, i(-1)); + let p = ar.corner(Iv::new(x.lo, hn), Iv::point(i(*a))); + inter.push(Inter { key: "leaky.p", class: Class::Acc, iv: p }); + r = Some(Iv::new(ar.shr(p.lo, *f), ar.shr(p.hi, *f))); + } else { + inter.push(Inter { key: "leaky.p", class: Class::Acc, iv: Iv::point(Z) }); + } + if !x.hi.is_neg() { + let nn = Iv::new(ar.max(x.lo, Z), x.hi); + r = hull_opt(r, nn); + } + vec![r.unwrap()] + } + (Params::Lut16 { t, .. }, _) => { + if x.lo < i(-32767) || x.hi > i(32767) { + lut_range = true; + vec![Iv::point(Z)] + } else { + let (l, h) = (x.lo.to_i64().unwrap(), x.hi.to_i64().unwrap()); + let mut mn = i64::MAX; + let mut mx = i64::MIN; + for v in l..=h { + let e = t[(v + 32768) as usize] as i64; + mn = mn.min(e); + mx = mx.max(e); + } + vec![Iv::of_i64(mn, mx)] + } + } + (Params::Igdn { gf, frac, gamma, beta, .. }, _) => { + let (l2, h2) = (ar.mul(x.lo, x.lo), ar.mul(x.hi, x.hi)); + let x2 = if x.lo <= Z && Z <= x.hi { Iv::new(Z, l2.max(h2)) } else { Iv::new(l2.min(h2), l2.max(h2)) }; + let c = cin; + let (mut gx, mut tm, mut nn, mut rr, mut xr, mut res) = (None, None, None, None, None, None); + for oi in 0..c { + let b = i(beta[oi]); + let (mut plo, mut phi) = (b.min(Z), b.max(Z)); + let (mut flo, mut fhi) = (b, b); + for j in 0..c { + let gv = i(gamma[oi * c + j]); + let g2 = ar.corner(Iv::point(gv), x2); + gx = hull_opt(gx, g2); + let t = Iv::new(ar.shr(g2.lo, *gf), ar.shr(g2.hi, *gf)); + tm = hull_opt(tm, t); + plo = ar.add(plo, t.lo.min(Z)); + phi = ar.add(phi, t.hi.max(Z)); + flo = ar.add(flo, t.lo); + fhi = ar.add(fhi, t.hi); + } + nn = hull_opt(nn, Iv::new(plo, phi)); + let (a, b2) = (ar.max(Z, flo), ar.max(Z, fhi)); + let r = Iv::new(ar.isqrt(a), ar.isqrt(b2)); + rr = hull_opt(rr, r); + let p = ar.corner(x, r); + xr = hull_opt(xr, p); + res = hull_opt(res, Iv::new(ar.shr(p.lo, *frac), ar.shr(p.hi, *frac))); + } + inter.push(Inter { key: "igdn.x2", class: Class::Wide, iv: x2 }); + inter.push(Inter { key: "igdn.gx2", class: Class::Product, iv: gx.unwrap() }); + inter.push(Inter { key: "igdn.term", class: Class::Wide, iv: tm.unwrap() }); + inter.push(Inter { key: "igdn.N", class: Class::Wide, iv: nn.unwrap() }); + inter.push(Inter { key: "igdn.r", class: Class::Acc, iv: rr.unwrap() }); + inter.push(Inter { key: "igdn.xr", class: Class::Wide, iv: xr.unwrap() }); + vec![res.unwrap()] + } + (Params::LayerNorm { eps, gamma, beta, .. }, _) => { + let cc = u(cin as u64); + let e = u(*eps); + let (l, h) = (x.lo, x.hi); + let s = Iv::new(ar.mul(cc, l.min(Z)), ar.mul(cc, h.max(Z))); + let half = ar.fdiv(cc, u(2)); + let cl = ar.mul(cc, l); + let ch = ar.mul(cc, h); + let m = Iv::new(ar.sub(cl, half), ar.add(ch, half)); + let d = Iv::new(ar.sub(l, h), ar.sub(h, l)); + let dd = ar.sub(h, l); + let sq = ar.mul(dd, dd); + let t = Iv::new(Z, ar.mul(cc, sq)); + let v = Iv::new(e, ar.add(sq, e)); + let num = I512::pow2(32); + let r0 = ar.isqrt(e); + let k32 = ar.fdiv(num, r0); + let k = Iv::new(Z, k32); + let ic = ar.isqrt_ceil(cc); + let r1 = ar.add(r0, I512::ONE); + let d1 = ar.mul(dd, k32); + let a0 = ar.mul(ic, num); + let a1 = ar.mul(a0, r1); + let na1 = ar.neg(a1); + let fl = ar.fdiv(na1, r0); + let d2 = ar.neg(fl); + let dmax = ar.min(d1, d2); + let dk = Iv::new(ar.neg(dmax), dmax); + let n1s = ar.shr(d1, 16); + let n1 = ar.add(n1s, I512::ONE); + let b0 = ar.mul(ic, I512::pow2(16)); + let b1 = ar.mul(b0, r1); + let b2 = ar.fdiv(b1, r0); + let nmax = ar.add(b2, I512::ONE); + let nn = ar.min(n1, nmax); + let n = Iv::new(ar.neg(nn), nn); + let (mut ng, mut res) = (None, None); + for c in 0..cin { + let gv = Iv::point(i(gamma[c])); + let p = ar.corner(n, gv); + ng = hull_opt(ng, p); + let (a, b) = (ar.shr(p.lo, 16), ar.shr(p.hi, 16)); + res = hull_opt(res, Iv::new(ar.add(a, i(beta[c])), ar.add(b, i(beta[c])))); + } + inter.push(Inter { key: "ln.S", class: Class::Wide, iv: s }); + inter.push(Inter { key: "ln.m", class: Class::Wide, iv: m }); + inter.push(Inter { key: "ln.d", class: Class::Acc, iv: d }); + inter.push(Inter { key: "ln.T", class: Class::Wide, iv: t }); + inter.push(Inter { key: "ln.v", class: Class::Wide, iv: v }); + inter.push(Inter { key: "rsq.num", class: Class::Acc, iv: Iv::point(num) }); + inter.push(Inter { key: "ln.k", class: Class::Acc, iv: k }); + inter.push(Inter { key: "ln.dk", class: Class::Wide, iv: dk }); + inter.push(Inter { key: "ln.n", class: Class::Acc, iv: n }); + inter.push(Inter { key: "ln.ng", class: Class::Wide, iv: ng.unwrap() }); + vec![res.unwrap()] + } + (Params::L2norm { eps }, _) => { + let (hh, ww) = size_of(sizes, op.ins[0]); + let l = u(hh * ww); + let e = u(*eps); + let (al, ah) = (ar.abs(x.lo), ar.abs(x.hi)); + let xm = ar.max(al, ah); + let x2 = ar.mul(xm, xm); + let lx2 = ar.mul(l, x2); + let t = Iv::new(Z, ar.add(lx2, e)); + let num = I512::pow2(32); + let r0 = ar.isqrt(e); + let k = Iv::new(Z, ar.fdiv(num, r0)); + inter.push(Inter { key: "l2.T", class: Class::Wide, iv: t }); + inter.push(Inter { key: "rsq.num", class: Class::Acc, iv: Iv::point(num) }); + inter.push(Inter { key: "l2.k", class: Class::Acc, iv: k }); + inter.push(Inter { key: "l2.vk", class: Class::Wide, iv: Iv::new(num.neg(), num) }); + vec![Iv::new(I512::pow2(16).neg(), I512::pow2(16))] + } + (_, SOFTMAX) => { + let wmax = u(size_of(sizes, op.ins[0]).1); + let diff = Iv::new(ar.sub(x.lo, x.hi), Z); + inter.push(Inter { key: "sm.diff", class: Class::Acc, iv: diff }); + let uv = Iv::new(Z, ar.sub(x.hi, x.lo)); + exp_rule(&mut ar, &mut inter, uv); + let sum = Iv::new(Z, ar.mul(wmax, u(65553))); + let a = ar.mul(u(65553), I512::pow2(16)); + let b0 = ar.mul(wmax, u(65553)); + let b = ar.fdiv(b0, u(2)); + let num = Iv::new(Z, ar.add(a, b)); + inter.push(Inter { key: "sm.sum", class: Class::Acc, iv: sum }); + inter.push(Inter { key: "sm.num", class: Class::Acc, iv: num }); + vec![Iv::new(Z, I512::pow2(16))] + } + (_, SIGMOID) => { + let uv = abs_image(&mut ar, x); + exp_rule(&mut ar, &mut inter, uv); + let num = Iv::new(Z, ar.mul(u(65553), I512::pow2(16))); + inter.push(Inter { key: "sig.num", class: Class::Acc, iv: num }); + vec![Iv::new(Z, I512::pow2(16))] + } + (Params::Elu { alpha }, _) => { + let (nh, nl) = (ar.neg(x.hi), ar.neg(x.lo)); + let uv = Iv::new(ar.max(Z, nh), ar.max(Z, nl)); + exp_rule(&mut ar, &mut inter, uv); + let p = ar.corner(Iv::point(i(*alpha)), Iv::new(I512::pow2(16).neg(), u(17))); + inter.push(Inter { key: "elu.p", class: Class::Wide, iv: p }); + let mut r: Option = None; + if x.hi > Z { + r = Some(Iv::new(ar.max(x.lo, I512::ONE), x.hi)); + } + if x.lo <= Z { + let q = Iv::new(ar.shr(p.lo, 16), ar.shr(p.hi, 16)); + r = hull_opt(r, q); + } + vec![r.unwrap()] + } + (_, CONCAT) | (_, SUBPOS_MERGE) => { + let mut r = x; + for &v in &ins[1..] { + r = r.hull(v); + } + vec![r] + } + (Params::Fill { kind, value }, _) => { + if *kind == 0 { + vec![Iv::point(i(*value))] + } else { + let s = g.scalars[*value as usize]; + vec![Iv::of_i64(s.lo, s.hi)] + } + } + // Every other operator moves its first input's values (`crop_to` included). + _ => vec![x; op.outs.len()], + }; + Eval { inter, results, saturated: ar.hit, lut_range } +} + +/// The image of an interval under `|·|` (§8.3). +fn abs_image(ar: &mut Ar, x: Iv) -> Iv { + if !x.lo.is_neg() { + x + } else if x.hi <= Z { + Iv::new(ar.neg(x.hi), ar.neg(x.lo)) + } else { + let nl = ar.neg(x.lo); + Iv::new(Z, ar.max(nl, x.hi)) + } +} + +fn fits32(v: i64) -> bool { + i32::try_from(v).is_ok() +} + +/// The parameter conditions of §7.2 a node fails, in that table's order. +fn params32(op: &Op) -> Vec<&'static str> { + let mut v = Vec::new(); + let rq_keys = |v: &mut Vec<&'static str>, rq: &Rq| { + if !rq.mult.iter().all(|&m| fits32(m)) { + v.push("rq.mult"); + } + if !rq.shift.iter().all(|&s| s <= 63) { + v.push("rq.shift"); + } + if rq.bias_kind == BiasKind::Post && !rq.bias.iter().all(|&b| fits32(b)) { + v.push("rq.post_bias"); + } + }; + match &op.p { + Params::Conv { rq, w, .. } | Params::ConvT { rq, w, .. } => { + rq_keys(&mut v, rq); + if !w.iter().all(|&x| fits32(x)) { + v.push("weights"); + } + } + Params::Rescale(rq) | Params::Mul(rq) | Params::Gram { rq, .. } | Params::Apply { rq, .. } => rq_keys(&mut v, rq), + Params::Chscale { shift, m, .. } => { + if !m.iter().all(|&x| fits32(x)) { + v.push("chscale.m"); + } + if *shift > 63 { + v.push("chscale.s"); + } + } + Params::Igdn { gf, frac, gamma, .. } => { + if !gamma.iter().all(|&x| fits32(x)) { + v.push("igdn.gamma"); + } + if *gf > 63 || *frac > 63 { + v.push("igdn.shift"); + } + } + Params::Leaky { a, .. } => { + if !fits32(*a) { + v.push("leaky.a"); + } + } + _ => {} + } + v +} + +/// The node's flags of §9.2, in ascending order. +fn npu_flags(g: &Graph, op: &Op) -> Vec<&'static str> { + let mut v: Vec<&'static str> = Vec::new(); + let rq_flags = |v: &mut Vec<&'static str>, round: Round, mult: Option<&[i64]>, bias_post: bool, shifts: &[u8]| { + if round == Round::HalfEven { + v.push("rq_half_even"); + } + if round == Round::Floor && mult.is_some() { + v.push("rq_floor_mult"); + } + if bias_post { + v.push("rq_post_bias"); + } + if mult.is_some_and(|m| m.iter().any(|&x| x < 0)) { + v.push("rq_neg_mult"); + } + if shifts.iter().any(|&s| s > 31) { + v.push("rq_shift_range"); + } + }; + match &op.p { + Params::Conv { rq, .. } | Params::ConvT { rq, .. } | Params::Rescale(rq) | Params::Mul(rq) | Params::Gram { rq, .. } | Params::Apply { rq, .. } => { + let m = if rq.m_kind == 0 { None } else { Some(rq.mult.as_slice()) }; + rq_flags(&mut v, rq.round, m, rq.bias_kind == BiasKind::Post, &rq.shift); + } + Params::Chscale { round, shift, m, .. } => { + v.push("chscale"); + rq_flags(&mut v, *round, Some(m.as_slice()), false, &[*shift]); + } + Params::Lut16 { .. } => v.push("lut16"), + _ => {} + } + if (op.code == ADD || op.code == CHUNK_ADD) && op.ins.iter().any(|&t| g.inputs.iter().any(|i| i.tensor == t && i.kind == 1)) { + v.push("state_add"); + } + v.sort_unstable_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + v.dedup(); + v +} + +/// `K_c` (§6) over a term interval: `None` when it is unbounded (`P = 0`). +fn k_c(term: &Iv) -> Option { + let p = term.lo.abs().max(term.hi.abs()); + if p.is_zero() { None } else { Some(I512::from_i64(i32::MAX as i64).div_floor(p)) } +} + +fn refuse8() -> Refusal { + Refusal::new(Code::ModelMismatch, Site::Load(8)) +} + +/// The pass over `g` as it stood (the oracle of [`crate::bounds::pass`]). +pub fn pass(g: &Graph, sizes: Option<&[(u64, u64)]>) -> Result { + pass_with(g, sizes, &[]) +} + +/// The pass with some tensors' intervals replaced, after their policy (for §8.4's +/// search and for `--advise`). +pub fn pass_with(g: &Graph, sizes: Option<&[(u64, u64)]>, over: &[(u32, Iv)]) -> Result { + let nt = g.tensors.len(); + let mut iv: Vec = vec![Iv::point(Z); nt]; + let set = |iv: &mut Vec, t: u32, v: Iv| { + iv[t as usize] = over.iter().rev().find(|(o, _)| *o == t).map_or(v, |(_, x)| *x); + }; + for inp in &g.inputs { + let t = &g.tensors[inp.tensor as usize]; + set(&mut iv, inp.tensor, Iv::of_i64(t.lo, t.hi)); + } + let mut nodes = Vec::with_capacity(g.ops.len()); + for (k, op) in g.ops.iter().enumerate() { + let ins: Vec = op.ins.iter().map(|&t| iv[t as usize]).collect(); + let e = eval(g, op, &ins, sizes); + if e.lut_range { + return Err(refuse8()); + } + let mut outs = Vec::with_capacity(op.outs.len()); + for (&t, r) in op.outs.iter().zip(&e.results) { + let rec = &g.tensors[t as usize]; + let (lo, hi) = (i(rec.lo), i(rec.hi)); + let after = match rec.policy { + Policy::Derived => *r, + Policy::Clamp => Iv::new(r.lo.max(lo).min(hi), r.hi.max(lo).min(hi)), + Policy::Check => { + let a = Iv::new(r.lo.max(lo), r.hi.min(hi)); + if a.lo > a.hi { + return Err(refuse8()); + } + a + } + }; + set(&mut iv, t, after); + outs.push((t, rec.policy, iv[t as usize])); + } + let term = e.inter.iter().find(|x| matches!(x.key, "conv.term" | "convT.term" | "igdn.term" | "gram.term" | "apply.term")).map(|x| x.iv); + let a_bits = match op.code { + CONV | CONV_TRANSPOSE | CHSCALE | MUL | GRAM | APPLY => Some(ins.iter().map(Iv::bits).collect()), + _ => None, + }; + let w_bits = match &op.p { + Params::Conv { w, .. } | Params::ConvT { w, .. } | Params::Chscale { m: w, .. } => { + let (mn, mx) = (w.iter().copied().min().unwrap_or(0), w.iter().copied().max().unwrap_or(0)); + Some(Iv::of_i64(mn, mx).bits()) + } + _ => None, + }; + nodes.push(Node { + index: k as u32, + code: op.code, + saturated: e.saturated, + inter: e.inter, + results: e.results, + outs, + params32: params32(op), + k_c: term.map(|t| k_c(&t)), + a_bits, + w_bits, + npu_flags: npu_flags(g, op), + }); + } + Ok(Bounds { nodes, tensors: iv }) +} + +#[cfg(test)] +mod tests { + //! The closed forms of [`crate::bounds`] against this oracle: on every committed + //! graph, on the pretrained conditional-codec set when its directory is given + //! (`WAI_SOTA_INT_DIR`), and on random operators and graphs built to reach what + //! the committed sets do not: weights at the ends of `i64`, input intervals at + //! `i128`'s ends and up to `2^255`, `l > h`, saturation, and every residue shape of + //! a transposed convolution. + //! + //! The random tests run a fixed count, or for `WAI_DIFF_SECS` seconds each, from + //! `WAI_DIFF_SEED` (or a fixed default); a failing case names its seed. + extern crate std; + + use std::path::PathBuf; + use std::string::String; + use std::time::{Duration, Instant}; + use std::vec::Vec; + use std::{eprintln, format, vec}; + + use super::{Eval, eval, pass}; + use crate::bounds::{self as nb, Bounds, Iv, Profile}; + use crate::code::Refusal; + use crate::generate::rng::Rng; + use crate::graph::{BiasKind, CAP, Graph, InputRec, Op, Params, Round, Rq, TensorRec, opcode::*}; + use crate::xint::I512; + + fn wai() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") + } + + /// Where two passes first differ, for the failure message. + fn first_difference(a: &Result, b: &Result) -> String { + match (a, b) { + (Ok(a), Ok(b)) => { + for (x, y) in a.nodes.iter().zip(&b.nodes) { + if x != y { + return format!("node {}:\n new {x:?}\n old {y:?}", x.index); + } + } + if a.nodes.len() != b.nodes.len() { + return format!("{} nodes, the oracle {}", a.nodes.len(), b.nodes.len()); + } + format!("tensors: new {:?}\n old {:?}", a.tensors, b.tensors) + } + _ => format!("new {:?}\n old {:?}", a.as_ref().err(), b.as_ref().err()), + } + } + + /// What one comparison saw: whether the pass refused, and which profiles the + /// verdict meets. + #[derive(Default)] + struct Seen { + graphs: u64, + refused: u64, + acc32: u64, + acc64_only: u64, + neither: u64, + } + + /// The pass and the oracle on `g`: the same result, whole (every node's + /// intermediates, results, saturation, parameter conditions, `K_c`, widths and + /// flags, and every tensor's interval), or the same refusal; and the same first + /// failure under each profile. + fn alike(g: &Graph, sizes: Option<&[(u64, u64)]>, seen: &mut Seen, what: &dyn Fn() -> String) { + let (new, old) = (nb::pass(g, sizes), pass(g, sizes)); + compare(g, &new, &old, seen, what); + } + + fn compare(g: &Graph, new: &Result, old: &Result, seen: &mut Seen, what: &dyn Fn() -> String) { + assert!(new == old, "{}: the pass and its oracle differ at {}", what(), first_difference(new, old)); + seen.graphs += 1; + match (new, old) { + (Ok(b), Ok(o)) => { + for p in Profile::ALL { + assert_eq!(b.first_failure(g, p), o.first_failure(g, p), "{}", what()); + } + match (b.meets(g, Profile::Acc32), b.meets(g, Profile::Acc64)) { + (true, _) => seen.acc32 += 1, + (false, true) => seen.acc64_only += 1, + _ => seen.neither += 1, + } + } + _ => seen.refused += 1, + } + } + + /// Every lowered graph of every committed set (`ops-conformance/certificates/`), and + /// every graph of the graph vectors (`ops-conformance/graphs/`) that parses and + /// plans. + #[test] + fn every_committed_graph_passes_as_the_oracle_does() { + let root = wai().join("ops-conformance"); + let mut seen = Seen::default(); + let (mut sets, mut vectors) = (0u64, 0u64); + let mut caps: Vec = std::fs::read_dir(root.join("certificates")).unwrap().map(|e| e.unwrap().path()).filter(|p| p.is_dir()).collect(); + caps.sort(); + for cap in caps { + let mut digests: Vec = std::fs::read_dir(&cap).unwrap().map(|e| e.unwrap().path()).filter(|p| p.is_dir()).collect(); + digests.sort(); + for d in digests { + let mut roles: Vec = std::fs::read_dir(&d).unwrap().map(|e| e.unwrap().path()).filter(|p| p.extension().is_some_and(|x| x == "whg1")).collect(); + roles.sort(); + for r in roles { + let b = std::fs::read(&r).unwrap(); + let g = Graph::parse(&b).unwrap_or_else(|e| panic!("{}: {e:?}", r.display())); + let sizes = crate::plan::load_plan(&g).unwrap(); + alike(&g, sizes.as_deref(), &mut seen, &|| format!("{}", r.display())); + sets += 1; + } + } + } + let mut files: Vec = std::fs::read_dir(root.join("graphs")).unwrap().map(|e| e.unwrap().path()).collect(); + files.sort(); + for f in files { + let text = std::fs::read_to_string(&f).unwrap(); + for (k, line) in text.lines().enumerate() { + let Some(at) = line.find("\"graph\":\"") else { continue }; + let rest = &line[at + 9..]; + let hex = &rest[..rest.find('"').unwrap()]; + let b = crate::json::unhex(hex).unwrap(); + let Ok(g) = Graph::parse(&b) else { continue }; + let Ok(sizes) = crate::plan::load_plan(&g) else { continue }; + alike(&g, sizes.as_deref(), &mut seen, &|| format!("{} line {}", f.display(), k + 1)); + vectors += 1; + } + } + eprintln!( + "committed: {sets} lowered graphs, {vectors} vector graphs; {} refused by the pass, {} meet acc32, {} acc64 only, {} neither", + seen.refused, seen.acc32, seen.acc64_only, seen.neither + ); + assert!(sets >= 30 && vectors >= 100, "the committed graphs were not all found: {sets} lowered, {vectors} vectors"); + } + + /// The pretrained conditional-codec set (`h_s` and `g_s`, lowered by `whm1/1`), when + /// `WAI_SOTA_INT_DIR` names the directory its vectors were generated into. + #[test] + fn the_pretrained_set_passes_as_the_oracle_does() { + let Ok(dir) = std::env::var("WAI_SOTA_INT_DIR") else { + eprintln!("WAI_SOTA_INT_DIR is not set: the pretrained set is not compared here"); + return; + }; + let rd = |n: &str| std::fs::read(PathBuf::from(&dir).join(n)).unwrap_or_else(|e| panic!("{n}: {e}")); + let mut seen = Seen::default(); + for (role, file, rgb8) in [("h_s", "int_hs_model.bin", false), ("g_s", "int_synth_sota_model.bin", true)] { + let g = crate::lower::whm1(&rd(file), rgb8).unwrap(); + let sizes = crate::plan::load_plan(&g).unwrap(); + let t = Instant::now(); + let new = nb::pass(&g, sizes.as_deref()); + let (tn, t) = (t.elapsed(), Instant::now()); + let old = pass(&g, sizes.as_deref()); + let to = t.elapsed(); + compare(&g, &new, &old, &mut seen, &|| String::from(role)); + eprintln!("pretrained {role}: the pass {tn:?}, the oracle {to:?}"); + } + assert_eq!((seen.graphs, seen.acc64_only), (2, 2)); + } + + /// Run `case` on successive seeds: `n` of them, or for `WAI_DIFF_SECS` seconds. + fn cases(name: &str, n: u64, mut case: impl FnMut(&mut Rng, u64)) -> u64 { + let secs: Option = std::env::var("WAI_DIFF_SECS").ok().and_then(|s| s.parse().ok()); + let seed0: u64 = std::env::var("WAI_DIFF_SEED").ok().and_then(|s| s.parse().ok()).unwrap_or(0xB0_0D5); + let until = secs.map(|s| Instant::now() + Duration::from_secs(s)); + let mut k = 0u64; + loop { + match until { + Some(t) if Instant::now() >= t => break, + None if k >= n => break, + _ => {} + } + let seed = seed0 ^ k.wrapping_mul(0xD1B5_4A32_D192_ED03); + case(&mut Rng::new(seed), seed); + k += 1; + } + eprintln!("{name}: {k} cases from WAI_DIFF_SEED={seed0}"); + k + } + + fn pow2(k: u32) -> I512 { + I512::pow2(k) + } + + /// A weight: 0, small, `i16`, `i32`, any `i64`, an end of `i64`, near `2^47` (where a + /// sum of `2^16` weights leaves `i64`), or near `2^62`. + fn weight(r: &mut Rng) -> i64 { + let s = if r.chance(1, 2) { 1 } else { -1 }; + match r.below(10) { + 0 | 1 => 0, + 2 => r.int(-16, 16), + 3 => r.int(i16::MIN as i64, i16::MAX as i64), + 4 => r.int(i32::MIN as i64, i32::MAX as i64), + 5 => r.next() as i64, + 6 => r.pick(&[i64::MIN, i64::MIN + 1, i64::MAX, i64::MAX - 1, -1, 1]), + 7 => s * ((1i64 << 47) + r.int(-2, 2)), + 8 => s * ((1i64 << 62) + r.int(-2, 2)), + _ => r.int(-(1 << 40), 1 << 40), + } + } + + /// `n` weights of one style: mixed, all `≥ 0`, all `≤ 0`, mostly 0, or all at one + /// end of `i64` (where a sum of two leaves `i64`). + fn weights(r: &mut Rng, n: usize) -> Vec { + let style = r.below(8); + let end = r.pick(&[i64::MAX, i64::MIN, i64::MAX - 1, i64::MIN + 1]); + (0..n) + .map(|_| { + let w = weight(r); + match style { + 0 => w.checked_abs().unwrap_or(i64::MAX), + 1 => -w.checked_abs().unwrap_or(i64::MAX), + 2 if r.chance(3, 4) => 0, + 3 => end, + _ => w, + } + }) + .collect() + } + + /// An interval end within `±2^255`: small, `i64`'s and `i128`'s ends and one past + /// them, `±2^k` and its neighbours, the saturation cap, and random wide values. + fn end(r: &mut Rng) -> I512 { + let s = r.chance(1, 2); + let v = match r.below(12) { + 0 => I512::ZERO, + 1 => I512::from_i64(r.int(-16, 16)), + 2 => I512::from_i64(r.int(i32::MIN as i64, i32::MAX as i64)), + 3 => I512::from_i64(r.pick(&[i64::MIN, i64::MIN + 1, i64::MAX, i64::MAX - 1])), + 4 => I512::from_i64(r.next() as i64), + 5 => I512::from_i128(r.pick(&[i128::MIN, i128::MIN + 1, i128::MAX, i128::MAX - 1])), + 6 => pow2(127).add(I512::from_i64(r.int(-1, 1))), + 7 => I512::from_i128((((r.next() as u128) << 64) | r.next() as u128) as i128), + 8 => { + let k = r.u(0, 254); + pow2(k).add(I512::from_i64(r.int(-1, 1))) + } + 9 => pow2(255).sub(I512::from_i64(r.int(0, 1))), + _ => I512::from_i64(r.int(-(1 << 40), 1 << 40)), + }; + if s { v.neg() } else { v } + } + + /// An interval, ordered except one time in sixteen (no pass forms `l > h`, but the + /// closed forms must not assume it). + fn interval(r: &mut Rng) -> Iv { + let (a, b) = (end(r), end(r)); + if r.chance(1, 16) { Iv::new(a, b) } else { Iv::new(a.min(b), a.max(b)) } + } + + /// A requantisation block for `c` channels: every rounding, bias kind, multiplier + /// kind and shift kind. + fn rq(r: &mut Rng, c: usize) -> Rq { + let round = r.pick(&[Round::Floor, Round::HalfUp, Round::HalfEven]); + let bias_kind = r.pick(&[BiasKind::None, BiasKind::Pre, BiasKind::Post]); + let m_kind = r.below(3) as u8; + let s_kind = 1 + r.below(2) as u8; + let mult = match m_kind { + 0 => vec![], + 1 => vec![weight(r)], + _ => (0..c).map(|_| weight(r)).collect(), + }; + let mut shift = |r: &mut Rng| r.pick(&[0u8, 1, 7, 16, 31, 32, 63, 64, 100, 127]); + let shift: Vec = if s_kind == 1 { vec![shift(r)] } else { (0..c).map(|_| shift(r)).collect() }; + let bias = if bias_kind == BiasKind::None { vec![] } else { (0..c).map(|_| weight(r)).collect() }; + Rq { round, bias_kind, m_kind, s_kind, m_bytes: if m_kind == 0 { 0 } else { 8 }, b_bytes: if bias_kind == BiasKind::None { 0 } else { 8 }, mult, shift, bias } + } + + /// A random `conv`, `conv_transpose` or `igdn` reading `cin` channels, and its + /// output channels: kernels 1 to 5, strides 1 to 6 (past the kernel too), groups 1, + /// `cin` or another divisor. + fn op_params(r: &mut Rng, cin: u32) -> (u8, Params, u32) { + match r.below(3) { + 0 => { + let divisors: Vec = (1..=cin).filter(|d| cin % d == 0).collect(); + let groups = r.pick(&divisors); + let cout = groups * (1 + r.below(3) as u32); + let (kh, kw) = (1 + r.below(4) as u32, 1 + r.below(4) as u32); + let n = (cout * (cin / groups) * kh * kw) as usize; + let p = Params::Conv { cout, kh, kw, sh: 1 + r.below(3) as u32, sw: 1 + r.below(3) as u32, ph: 0, pw: 0, groups, w_bytes: 8, rq: rq(r, cout as usize), w: weights(r, n) }; + (CONV, p, cout) + } + 1 => { + let cout = 1 + r.below(3) as u32; + let (kh, kw) = (1 + r.below(5) as u32, 1 + r.below(5) as u32); + let (sh, sw) = (1 + r.below(6) as u32, 1 + r.below(6) as u32); + let n = (cin * cout * kh * kw) as usize; + let p = Params::ConvT { cout, kh, kw, sh, sw, ph: 0, pw: 0, oph: 0, opw: 0, w_bytes: 8, rq: rq(r, cout as usize), w: weights(r, n) }; + (CONV_TRANSPOSE, p, cout) + } + _ => { + let c = cin as usize; + let gf = r.pick(&[0u32, 1, 8, 16, 31, 63, 64, 100, 126, 127]); + let frac = r.pick(&[0u32, 1, 12, 30, 63, 127]); + let p = Params::Igdn { gf, frac, g_bytes: 8, gamma: weights(r, c * c), beta: (0..c).map(|_| weight(r)).collect() }; + (IGDN, p, cin) + } + } + } + + fn input(t: u32) -> InputRec { + InputRec { tensor: t, max_h: 0, max_w: 0, kind: 0, src: 0, init: 0 } + } + + /// One operator's rules against the oracle's, on random parameters and on input + /// intervals the pass itself never forms as well as those it does. + #[test] + fn each_operator_evaluates_as_the_oracle_does() { + let (mut closed, mut wide) = (0u64, 0u64); + let n = cases("operator", 4000, |r, seed| { + let cin = 1 + r.below(4) as u32; + let (code, p, cout) = op_params(r, cin); + let g = Graph { + max_elems: CAP as u32, + scalars: vec![], + inputs: vec![input(0)], + outputs: vec![1], + tensors: vec![TensorRec::check(cin, 0, 0), TensorRec::derived(cout)], + ops: vec![Op { code, ins: vec![0], outs: vec![1], p }], + }; + let x = interval(r); + let (a, b) = (nb::eval(&g, &g.ops[0], &[x], None), eval(&g, &g.ops[0], &[x], None)); + let old = Eval { inter: b.inter, results: b.results, saturated: b.saturated, lut_range: b.lut_range }; + let new = Eval { inter: a.inter, results: a.results, saturated: a.saturated, lut_range: a.lut_range }; + assert!(new == old, "seed {seed}: {:?} over {x:?}\n new {new:?}\n old {old:?}", g.ops[0]); + if x.lo.to_i128().is_some() && x.hi.to_i128().is_some() && x.lo <= x.hi { closed += 1 } else { wide += 1 } + }); + eprintln!("operator: {n} cases; {closed} inputs in i128 (the closed forms), {wide} wider or l > h (folded tap by tap)"); + assert!(closed > n / 4 && wide > n / 8, "both forms must be reached: {closed} and {wide} of {n}"); + } + + /// Random graphs of 1 to 6 operators: `conv`, `conv_transpose` and `igdn` among + /// `mul`s of a tensor by itself (which carry intervals past `i64`, past `i128` and to + /// saturation), `add`s, `relu`s, `clamp`s and `rescale`s, with outputs `derived`, + /// `clamp` or `check` (which can refuse the graph). + #[test] + fn random_graphs_pass_as_the_oracle_does() { + let mut seen = Seen::default(); + let mut saturated = 0u64; + let n = cases("graph", 1500, |r, seed| { + let c0 = 1 + r.below(4) as u32; + let (lo, hi) = { + let (a, b) = (weight(r), weight(r)); + (a.min(b), a.max(b)) + }; + let mut tensors = vec![TensorRec::check(c0, lo, hi)]; + let mut ops = Vec::new(); + let mut cur = 0u32; + for _ in 0..1 + r.below(6) { + let c = tensors[cur as usize].channels; + let (code, ins, p, cout) = match r.below(9) { + 0..=2 => { + let (code, p, cout) = op_params(r, c); + (code, vec![cur], p, cout) + } + 3 | 4 => { + let mut q = rq(r, c as usize); + (q.bias_kind, q.b_bytes, q.bias) = (BiasKind::None, 0, vec![]); + (MUL, vec![cur, cur], Params::Mul(q), c) + } + 5 => (ADD, vec![cur, cur], Params::None, c), + 6 => (RELU, vec![cur], Params::None, c), + 7 => { + let (a, b) = (weight(r), weight(r)); + (CLAMP, vec![cur], Params::Clamp { lo: a.min(b), hi: a.max(b) }, c) + } + _ => (RESCALE, vec![cur], Params::Rescale(rq(r, c as usize)), c), + }; + let out = match r.below(8) { + 0 | 1 => { + let (a, b) = (weight(r), weight(r)); + if r.chance(1, 2) { TensorRec::clamp(cout, a.min(b), a.max(b)) } else { TensorRec::check(cout, a.min(b), a.max(b)) } + } + _ => TensorRec::derived(cout), + }; + let t = tensors.len() as u32; + tensors.push(out); + ops.push(Op { code, ins, outs: vec![t], p }); + cur = t; + } + let g = Graph { max_elems: CAP as u32, scalars: vec![], inputs: vec![input(0)], outputs: vec![cur], tensors, ops }; + alike(&g, None, &mut seen, &|| format!("seed {seed}")); + if let Ok(b) = nb::pass(&g, None) { + saturated += b.nodes.iter().any(|n| n.saturated) as u64; + } + }); + eprintln!( + "graph: {n} graphs; {} refused by the pass, {} meet acc32, {} acc64 only, {} neither; {saturated} with a saturated node", + seen.refused, seen.acc32, seen.acc64_only, seen.neither + ); + assert!(seen.refused > 0 && seen.acc32 > 0 && seen.acc64_only > 0 && seen.neither > 0 && saturated > 0, "every outcome must be reached"); + } +} diff --git a/wai/wai-ops/src/lib.rs b/wai/wai-ops/src/lib.rs index 598c589e1..5eb008fe2 100644 --- a/wai/wai-ops/src/lib.rs +++ b/wai/wai-ops/src/lib.rs @@ -11,6 +11,8 @@ //! - [`limb32`]: a reference model of a 32-bit fabric's `acc32` products and epilogue. //! - [`bounds`]: load step 8, the bounds pass (§8.1–§8.3), the verdict and the bounds //! certificate (§8.5), and admissible ranges (§8.4). +//! - `bounds_oracle` (test-only, or the `oracle` feature): the bounds pass as it stood +//! before its closed forms, every term folded in 512 bits, which [`bounds`] is held to. //! - [`vectors`]: the conformance vector files of §14.1, recomputed from their inputs. //! - [`generate`]: the generator of `../ops-conformance/`, which writes those files. //! @@ -28,6 +30,8 @@ extern crate alloc; pub mod bounds; +#[cfg(any(test, feature = "oracle"))] +pub mod bounds_oracle; pub mod code; pub mod generate; pub mod graph; -- 2.54.0 (Apple Git-157) From db3be852c48aef2b3ca62aca89d4e2f67fbec9c9 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:30:57 -0400 Subject: [PATCH 02/22] wai-ops: the oracle test's floor on committed vector graphs is the 81 that parse and plan Co-Authored-By: Claude Opus 5.5 --- wai/wai-ops/src/bounds_oracle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wai/wai-ops/src/bounds_oracle.rs b/wai/wai-ops/src/bounds_oracle.rs index 42ba333b7..c3fa249b3 100644 --- a/wai/wai-ops/src/bounds_oracle.rs +++ b/wai/wai-ops/src/bounds_oracle.rs @@ -821,7 +821,7 @@ mod tests { "committed: {sets} lowered graphs, {vectors} vector graphs; {} refused by the pass, {} meet acc32, {} acc64 only, {} neither", seen.refused, seen.acc32, seen.acc64_only, seen.neither ); - assert!(sets >= 30 && vectors >= 100, "the committed graphs were not all found: {sets} lowered, {vectors} vectors"); + assert!(sets >= 30 && vectors >= 80, "the committed graphs were not all found: {sets} lowered, {vectors} vectors"); } /// The pretrained conditional-codec set (`h_s` and `g_s`, lowered by `whm1/1`), when -- 2.54.0 (Apple Git-157) From e1d08aea0f02e361ae7e6add8308c93f4e6ea39d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:32:10 -0400 Subject: [PATCH 03/22] wai-ops: the lowering's largest magnitude in u64, and the WHG1 writer in place maxabs finds the largest |x| over u64 and widens it once, rather than forming an I512 per weight. The writer reserves each integer list and writes the usual widths as the unsigned integer of that width (the same low bytes), and writes each operator's parameters in place after their length, rather than into a second vector copied after. Same bytes; a unit test holds each width to the low bytes it wrote before. Co-Authored-By: Claude Opus 5.5 --- wai/wai-ops/src/graph.rs | 92 ++++++++++++++++++++++++------------ wai/wai-ops/src/lower/whm.rs | 4 +- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/wai/wai-ops/src/graph.rs b/wai/wai-ops/src/graph.rs index f083b53f1..474d94454 100644 --- a/wai/wai-ops/src/graph.rs +++ b/wai/wai-ops/src/graph.rs @@ -857,9 +857,12 @@ impl Graph { for t in op.ins.iter().chain(&op.outs) { o.extend_from_slice(&t.to_le_bytes()); } - let p = write_params(&op.p); - o.extend_from_slice(&(p.len() as u32).to_le_bytes()); - o.extend_from_slice(&p); + // The parameters' length, then the parameters, written in place. + let at = o.len(); + o.extend_from_slice(&[0; 4]); + write_params_to(&mut o, &op.p); + let n = (o.len() - at - 4) as u32; + o[at..at + 4].copy_from_slice(&n.to_le_bytes()); } o } @@ -899,9 +902,16 @@ impl Graph { } } +/// Each of `v` as its low `w` bytes, little-endian. The usual widths are written a value +/// at a time as the unsigned integer of that width (its low bytes), after one reservation. fn put_ints(o: &mut Vec, v: &[i64], w: u8) { - for &x in v { - o.extend_from_slice(&x.to_le_bytes()[..w as usize]); + o.reserve(v.len().saturating_mul(w as usize)); + match w { + 1 => o.extend(v.iter().map(|&x| x as u8)), + 2 => v.iter().for_each(|&x| o.extend_from_slice(&(x as u16).to_le_bytes())), + 4 => v.iter().for_each(|&x| o.extend_from_slice(&(x as u32).to_le_bytes())), + 8 => v.iter().for_each(|&x| o.extend_from_slice(&x.to_le_bytes())), + _ => v.iter().for_each(|&x| o.extend_from_slice(&x.to_le_bytes()[..w as usize])), } } @@ -930,30 +940,36 @@ fn write_operand(o: &mut Vec, x: Operand) { /// One operator's parameter bytes (§10.4). pub fn write_params(p: &Params) -> Vec { let mut o = Vec::new(); + write_params_to(&mut o, p); + o +} + +/// [`write_params`], appended to `o`. +fn write_params_to(o: &mut Vec, p: &Params) { let u = |o: &mut Vec, v: u32| o.extend_from_slice(&v.to_le_bytes()); match p { Params::Conv { cout, kh, kw, sh, sw, ph, pw, groups, w_bytes, rq, w } => { for v in [*cout, *kh, *kw, *sh, *sw, *ph, *pw, *groups] { - u(&mut o, v); + u(o, v); } o.extend_from_slice(&[*w_bytes, 0, 0, 0]); - write_rq(&mut o, rq); - put_ints(&mut o, w, *w_bytes); + write_rq(o, rq); + put_ints(o, w, *w_bytes); } Params::ConvT { cout, kh, kw, sh, sw, ph, pw, oph, opw, w_bytes, rq, w } => { for v in [*cout, *kh, *kw, *sh, *sw, *ph, *pw, *oph, *opw] { - u(&mut o, v); + u(o, v); } o.extend_from_slice(&[*w_bytes, 0, 0, 0]); - write_rq(&mut o, rq); - put_ints(&mut o, w, *w_bytes); + write_rq(o, rq); + put_ints(o, w, *w_bytes); } - Params::Rescale(rq) | Params::Mul(rq) => write_rq(&mut o, rq), + Params::Rescale(rq) | Params::Mul(rq) => write_rq(o, rq), Params::Chscale { scalar, n_rows, round, m_bytes, shift, m } => { - u(&mut o, *scalar); - u(&mut o, *n_rows); + u(o, *scalar); + u(o, *n_rows); o.extend_from_slice(&[round_code(*round), *m_bytes, *shift, 0]); - put_ints(&mut o, m, *m_bytes); + put_ints(o, m, *m_bytes); } Params::Lut16 { e_in, e_out, t } => { o.extend_from_slice(&[*e_in as u8, *e_out as u8, 0, 0]); @@ -963,22 +979,22 @@ pub fn write_params(p: &Params) -> Vec { } Params::None => {} Params::Split { start, count } => { - u(&mut o, *start); - u(&mut o, *count); + u(o, *start); + u(o, *count); } - Params::Shuffle { r } => u(&mut o, *r), + Params::Shuffle { r } => u(o, *r), Params::Clamp { lo, hi } => { o.extend_from_slice(&lo.to_le_bytes()); o.extend_from_slice(&hi.to_le_bytes()); } Params::Leaky { a, f } => { o.extend_from_slice(&a.to_le_bytes()); - u(&mut o, *f); + u(o, *f); } - Params::PadRep { m } => u(&mut o, *m), + Params::PadRep { m } => u(o, *m), Params::Crop { y0, x0, h, w } => { for x in [*y0, *x0, *h, *w] { - write_operand(&mut o, x); + write_operand(o, x); } } Params::Subpos { order } => o.extend_from_slice(order), @@ -987,26 +1003,25 @@ pub fn write_params(p: &Params) -> Vec { o.extend_from_slice(&value.to_le_bytes()); } Params::Igdn { gf, frac, g_bytes, gamma, beta } => { - u(&mut o, *gf); - u(&mut o, *frac); + u(o, *gf); + u(o, *frac); o.extend_from_slice(&[*g_bytes, 0, 0, 0, 0, 0, 0, 0]); - put_ints(&mut o, gamma, *g_bytes); - put_ints(&mut o, beta, 8); + put_ints(o, gamma, *g_bytes); + put_ints(o, beta, 8); } Params::LayerNorm { eps, g_bytes, gamma, beta } => { o.extend_from_slice(&eps.to_le_bytes()); o.extend_from_slice(&[*g_bytes, 0, 0, 0, 0, 0, 0, 0]); - put_ints(&mut o, gamma, *g_bytes); - put_ints(&mut o, beta, 8); + put_ints(o, gamma, *g_bytes); + put_ints(o, beta, 8); } Params::L2norm { eps } => o.extend_from_slice(&eps.to_le_bytes()), Params::Elu { alpha } => o.extend_from_slice(&alpha.to_le_bytes()), Params::Gram { groups, rq } | Params::Apply { groups, rq } => { - u(&mut o, *groups); - write_rq(&mut o, rq); + u(o, *groups); + write_rq(o, rq); } } - o } /// The smallest width of `allowed` (ascending) that holds every value of `v`. @@ -1109,6 +1124,23 @@ mod tests { assert_eq!(Graph::parse(&b).unwrap().to_bytes(), b); } + /// The writer's integer lists: each value's low `w` bytes, little-endian, for every + /// width (the usual ones written as the unsigned integer of that width, the others + /// byte by byte), at the ends of each width and past them. + #[test] + fn put_ints_writes_each_values_low_bytes() { + let v = [0, 1, -1, 127, -128, 128, 255, 32767, -32768, 65535, i32::MAX as i64, i32::MIN as i64, u32::MAX as i64, i64::MAX, i64::MIN, 0x0123_4567_89ab_cdef]; + for w in [0u8, 1, 2, 3, 4, 5, 6, 7, 8] { + let mut o = vec![0xAA]; + put_ints(&mut o, &v, w); + let mut want = vec![0xAA]; + for x in v { + want.extend_from_slice(&x.to_le_bytes()[..w as usize]); + } + assert_eq!(o, want, "width {w}"); + } + } + /// The heap bytes are every list's capacity times its element's size: the records, /// the operators, their operand lists, and a parameter's weights and requantisation /// lists at the widths they are held in (`i64`, `u8`). diff --git a/wai/wai-ops/src/lower/whm.rs b/wai/wai-ops/src/lower/whm.rs index e1db5a231..e27552d51 100644 --- a/wai/wai-ops/src/lower/whm.rs +++ b/wai/wai-ops/src/lower/whm.rs @@ -131,8 +131,10 @@ pub fn parse_wam1(b: &[u8]) -> Res { parse(b, b"WAM1", true) } +/// The largest magnitude in `v` (0 for none). Every magnitude fits `u64` (`|i64::MIN|` +/// is `2^63`), so the largest is found there and widened once. fn maxabs(v: &[i64]) -> I512 { - v.iter().map(|&x| I512::from_i128((x as i128).abs())).max().unwrap_or(I512::ZERO) + I512::from_u64(v.iter().map(|&x| x.unsigned_abs()).max().unwrap_or(0)) } const I64: I512 = I512::from_i128(i64::MAX as i128); -- 2.54.0 (Apple Git-157) From aa328ffccb5be7fd2167a478a8770db25afb53a0 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:40:05 -0400 Subject: [PATCH 04/22] wai-ops, wai-rs: a lowered network loads without a second write and parse of its graph Builder::finish already writes the graph's bytes, parses them back to an equal graph and plans it; it now returns that parsed graph, the one graph::load of those bytes holds. graph::load_lowered (crate-only) makes load steps 7 and 8 on it, so whm1_net_loaded and wam1_net_loaded give load's result, refusal for refusal, without writing and parsing the bytes a second time. The WHM1 and WAM1 lowerings move each layer's lists into the graph instead of cloning them (whm1_net and wam1_net, which borrow, clone the network once). The int_hyper, splat and audio drivers load through them. Co-Authored-By: Claude Opus 5.5 --- wai/wai-ops/src/graph.rs | 11 +++- wai/wai-ops/src/lower/mod.rs | 19 ++++--- wai/wai-ops/src/lower/whm.rs | 89 +++++++++++++++++++++---------- wai/wai-rs/src/int_audio.rs | 4 +- wai/wai-rs/src/int_hyper_synth.rs | 12 ++++- wai/wai-rs/src/int_splat.rs | 4 +- 6 files changed, 93 insertions(+), 46 deletions(-) diff --git a/wai/wai-ops/src/graph.rs b/wai/wai-ops/src/graph.rs index 474d94454..b50d17ddb 100644 --- a/wai/wai-ops/src/graph.rs +++ b/wai/wai-ops/src/graph.rs @@ -1095,7 +1095,16 @@ impl core::ops::Deref for Loaded { /// driver that loads every graph of a set under its capability's declared profile /// refuses exactly the sets whose certificate does not meet it. pub fn load(b: &[u8], p: Profile) -> Res { - let g = Graph::parse(b)?; + load_lowered(Graph::parse(b)?, p) +} + +/// Load steps 7 and 8, for a graph that steps 1–6 gave: [`load`] after its parse, and a +/// lowering of this crate on the graph its `finish` returned. That `finish` wrote the +/// graph's bytes, parsed them back to this same graph and planned it, so `load` of those +/// bytes would parse it again, and step 7 and step 8 then see the same graph: the result +/// is `load`'s, refusal for refusal. Crate-only, so that nothing else loads a graph +/// around steps 1–6. +pub(crate) fn load_lowered(g: Graph, p: Profile) -> Res { let sizes = crate::plan::load_plan(&g)?; let bd = crate::bounds::step8(&g, sizes.as_deref(), p)?; let acc32 = bd.meets(&g, Profile::Acc32); diff --git a/wai/wai-ops/src/lower/mod.rs b/wai/wai-ops/src/lower/mod.rs index 271c6d8c6..430e9d7bf 100644 --- a/wai/wai-ops/src/lower/mod.rs +++ b/wai/wai-ops/src/lower/mod.rs @@ -16,7 +16,7 @@ mod whm; pub use jpegai::jpegai; pub use synth::int_synth; -pub use whm::{Net, NetLayer, int_hyper, parse_wam1, parse_whm1, splat, wam1, wam1_net, whm1, whm1_net, wmb1, world}; +pub use whm::{Net, NetLayer, int_hyper, parse_wam1, parse_whm1, splat, wam1, wam1_net, wam1_net_loaded, whm1, whm1_net, whm1_net_loaded, wmb1, world}; #[cfg(kani)] pub(crate) use whm::{p_conv, p_igdn, p_leaky}; @@ -73,17 +73,20 @@ impl Builder { self.tensors[t as usize].channels } /// The graph with `outputs`, after checking that it loads (steps 1–7): a lowering - /// writes only graphs that parse and plan. + /// writes only graphs that parse and plan. The graph returned is the one parsed back + /// from its bytes, equal to the one built: the graph [`crate::graph::load`] of those + /// bytes holds. pub fn finish(self, outputs: Vec) -> Res { let g = Graph { max_elems: CAP as u32, scalars: self.scalars, inputs: self.inputs, outputs, tensors: self.tensors, ops: self.ops }; let b = g.to_bytes(); - match Graph::parse(&b) { - Ok(back) if back == g => {} + let back = match Graph::parse(&b) { + Ok(back) if back == g => back, _ => return refuse(), - } - crate::plan::load_plan(&g).map_err(|_| Refusal::new(Code::ModelMismatch, Site::Lowering))?; - debug_assert!(g.ops.iter().all(|o| op_name(o.code).is_some())); - Ok(g) + }; + drop((b, g)); + crate::plan::load_plan(&back).map_err(|_| Refusal::new(Code::ModelMismatch, Site::Lowering))?; + debug_assert!(back.ops.iter().all(|o| op_name(o.code).is_some())); + Ok(back) } } diff --git a/wai/wai-ops/src/lower/whm.rs b/wai/wai-ops/src/lower/whm.rs index e27552d51..e4bf12325 100644 --- a/wai/wai-ops/src/lower/whm.rs +++ b/wai/wai-ops/src/lower/whm.rs @@ -4,7 +4,8 @@ use alloc::vec; use alloc::vec::Vec; use super::{Builder, Lowered, Res, check_x, largest, refuse}; -use crate::graph::{BiasKind, Graph, Operand, Params, Round, Rq, TensorRec, opcode}; +use crate::bounds::Profile; +use crate::graph::{BiasKind, Graph, Loaded, Operand, Params, Round, Rq, TensorRec, opcode}; use crate::xint::I512; /// One layer of a `WHM1` or `WAM1` network, as the file holds it. @@ -229,11 +230,16 @@ fn x_record(net: &Net, l: &NetLayer, c: u32, network_input: bool) -> Res Res { +/// +/// Each layer's lists move into its operator's parameters, so the network's layers are +/// left without them: a layer's lists are read only to write its own operator, and its +/// record (`X_ℓ`) is formed from them one layer earlier, before they move. +fn layers(b: &mut Builder, net: &mut Net, x0: u32) -> Res { + let (shift, audio, gf, afrac) = (net.shift, net.audio, net.gf, net.afrac); let mut x = x0; - for (i, l) in net.layers.iter().enumerate() { + for i in 0..net.layers.len() { let c = b.channels(x); - let out_c = match l { + let out_c = match &net.layers[i] { NetLayer::ConvT { out, .. } | NetLayer::Conv { out, .. } => *out, _ => c, }; @@ -241,26 +247,27 @@ fn layers(b: &mut Builder, net: &Net, x0: u32) -> Res { Some(next) => x_record(net, next, out_c, false)?, None => TensorRec::derived(out_c), }; - let rq = |mult: &[i64], bias: &[i64]| Rq { + let rq = |mult: Vec, bias: Vec| Rq { round: Round::Floor, bias_kind: BiasKind::Post, m_kind: 2, s_kind: 1, m_bytes: 8, b_bytes: 8, - mult: mult.to_vec(), - shift: vec![net.shift as u8], - bias: bias.to_vec(), + mult, + shift: vec![shift as u8], + bias, }; - x = match l { + let take = core::mem::take::>; + x = match &mut net.layers[i] { NetLayer::ConvT { inc, out, k, stride, pad, opad, w, mult, bias } => { if *inc != c || *out == 0 || *k == 0 || *stride == 0 { return refuse(); } - let p = if net.audio { - Params::ConvT { cout: *out, kh: 1, kw: *k, sh: 1, sw: *stride, ph: 0, pw: *pad, oph: 0, opw: *opad, w_bytes: 2, rq: rq(mult, bias), w: w.clone() } + let p = if audio { + Params::ConvT { cout: *out, kh: 1, kw: *k, sh: 1, sw: *stride, ph: 0, pw: *pad, oph: 0, opw: *opad, w_bytes: 2, rq: rq(take(mult), take(bias)), w: take(w) } } else { - Params::ConvT { cout: *out, kh: *k, kw: *k, sh: *stride, sw: *stride, ph: *pad, pw: *pad, oph: *opad, opw: *opad, w_bytes: 2, rq: rq(mult, bias), w: w.clone() } + Params::ConvT { cout: *out, kh: *k, kw: *k, sh: *stride, sw: *stride, ph: *pad, pw: *pad, oph: *opad, opw: *opad, w_bytes: 2, rq: rq(take(mult), take(bias)), w: take(w) } }; b.op(opcode::CONV_TRANSPOSE, vec![x], rec, p) } @@ -268,14 +275,14 @@ fn layers(b: &mut Builder, net: &Net, x0: u32) -> Res { if *inc != c || *out == 0 || *k == 0 || *stride == 0 { return refuse(); } - let p = Params::Conv { cout: *out, kh: *k, kw: *k, sh: *stride, sw: *stride, ph: *pad, pw: *pad, groups: 1, w_bytes: 2, rq: rq(mult, bias), w: w.clone() }; + let p = Params::Conv { cout: *out, kh: *k, kw: *k, sh: *stride, sw: *stride, ph: *pad, pw: *pad, groups: 1, w_bytes: 2, rq: rq(take(mult), take(bias)), w: take(w) }; b.op(opcode::CONV, vec![x], rec, p) } NetLayer::Igdn { c: cc, gamma, beta } => { if *cc != c { return refuse(); } - let p = Params::Igdn { gf: net.gf, frac: net.afrac, g_bytes: 4, gamma: gamma.clone(), beta: beta.clone() }; + let p = Params::Igdn { gf, frac: afrac, g_bytes: 4, gamma: take(gamma), beta: take(beta) }; b.op(opcode::IGDN, vec![x], rec, p) } NetLayer::Relu => b.op(opcode::RELU, vec![x], rec, Params::None), @@ -299,34 +306,48 @@ fn rgb8(b: &mut Builder, x: u32, afrac: u32) -> u32 { b.op(opcode::RESCALE, vec![t], TensorRec::derived(c), Params::Rescale(rq)) } -fn net_graph(net: &Net, output_step: bool) -> Res { - header_ok(net)?; - let c0 = in_channels(net)?; +/// The graph of `net` (its lists move into the graph). +fn net_graph(mut net: Net, output_step: bool) -> Res { + header_ok(&net)?; + let c0 = in_channels(&net)?; let mut b = Builder::new(); let first = net.layers.first().ok_or(()).or_else(|_| refuse())?; - let x0 = b.input(x_record(net, first, c0, true)?, 0, 0); - let mut x = layers(&mut b, net, x0)?; + let x0 = b.input(x_record(&net, first, c0, true)?, 0, 0); + let mut x = layers(&mut b, &mut net, x0)?; if output_step { x = rgb8(&mut b, x, net.afrac); } + drop(net); b.finish(vec![x]) } /// `whm1/1`: one `WHM1` network, with the RGB8 step when `output_step` is set. pub fn whm1(bytes: &[u8], output_step: bool) -> Res { - whm1_net(&parse_whm1(bytes)?, output_step) + whm1_owned(parse_whm1(bytes)?, output_step) } /// [`whm1`] of a network already parsed: a decoder that holds its network parsed lowers /// it from that form, which is [`parse_whm1`] of its bytes. The graph is the one /// [`whm1`] writes from those bytes. pub fn whm1_net(net: &Net, output_step: bool) -> Res { + whm1_owned(net.clone(), output_step) +} + +fn whm1_owned(net: Net, output_step: bool) -> Res { if net.audio { return refuse(); } net_graph(net, output_step) } +/// [`whm1_net`], then the graph loaded under `p`: the same result, refusal for refusal, +/// as [`crate::graph::load`] of the graph's bytes, without writing and parsing those +/// bytes again, which the lowering has just done (see [`crate::graph::load_lowered`]). +/// The network's lists move into the graph. +pub fn whm1_net_loaded(net: Net, output_step: bool, p: Profile) -> Res { + crate::graph::load_lowered(whm1_owned(net, output_step)?, p) +} + /// `whm1/1` for `wai.neural.int_hyper` and the keyframe of `wai.video.int_hyper`: /// `h_s`, then `g_s` with the RGB8 step. pub fn int_hyper(h_s: &[u8], g_s: &[u8]) -> Res { @@ -340,23 +361,33 @@ pub fn splat(g_s: &[u8]) -> Res { /// `wam1/1` (§13.3): the `WAM1` synthesis with the PCM step. pub fn wam1(bytes: &[u8]) -> Res { - wam1_net(&parse_wam1(bytes)?) + wam1_owned(parse_wam1(bytes)?) } /// [`wam1`] of a network already parsed: a decoder that holds its network parsed lowers /// it from that form, which is [`parse_wam1`] of its bytes. The graph is the one [`wam1`] /// writes from those bytes. pub fn wam1_net(net: &Net) -> Res { + wam1_owned(net.clone()) +} + +/// [`wam1_net`], then the graph loaded under `p`, as [`whm1_net_loaded`] does. +pub fn wam1_net_loaded(net: Net, p: Profile) -> Res { + crate::graph::load_lowered(wam1_owned(net)?, p) +} + +/// The graph of `net` (its lists move into the graph). +fn wam1_owned(mut net: Net) -> Res { if !net.audio { return refuse(); } - header_ok(net)?; - let c0 = in_channels(net)?; + header_ok(&net)?; + let c0 = in_channels(&net)?; let mut b = Builder::new(); let first = net.layers.first().ok_or(()).or_else(|_| refuse())?; - let x0 = b.input(x_record(net, first, c0, true)?, 0, 0); + let x0 = b.input(x_record(&net, first, c0, true)?, 0, 0); let t_out = b.scalar(1, 1 << 26); - let x = layers(&mut b, net, x0)?; + let x = layers(&mut b, &mut net, x0)?; let c = b.channels(x); // The range in which v·32767 + 2^(afrac−1) fits i64. let lo = -((1i128 << 63) / 32767) as i64; // ⌈−2^63 / 32767⌉ @@ -394,13 +425,13 @@ pub fn wmb1(bytes: &[u8]) -> Res { Some(a) if a == gn.afrac && tables.as_obj().is_some() => {} _ => return refuse(), } - Ok(Lowered { roles: vec![("h_s", net_graph(&hn, false)?), ("entropy", net_graph(&en, false)?), ("g_s", net_graph(&gn, true)?)] }) + Ok(Lowered { roles: vec![("h_s", net_graph(hn, false)?), ("entropy", net_graph(en, false)?), ("g_s", net_graph(gn, true)?)] }) } /// `world/1` (§13.5, informative): the transition `T` with `A` action planes, then /// the synthesis `g_s` with the RGB8 step. `c` is the latent's channel count. pub fn world(t: &[u8], g_s: &[u8], c: u32) -> Res { - let tn = parse_whm1(t)?; + let mut tn = parse_whm1(t)?; let gn = parse_whm1(g_s)?; header_ok(&tn)?; header_ok(&gn)?; @@ -421,10 +452,10 @@ pub fn world(t: &[u8], g_s: &[u8], c: u32) -> Res { } let first = tn.layers.first().ok_or(()).or_else(|_| refuse())?; let x0 = b.op(opcode::CONCAT, parts, x_record(&tn, first, cin, true)?, Params::None); - let x = layers(&mut b, &tn, x0)?; + let x = layers(&mut b, &mut tn, x0)?; let one = 1i64 << tn.afrac; let cc = b.channels(x); let out = b.op(opcode::CLAMP, vec![x], TensorRec::derived(cc), Params::Clamp { lo: -one, hi: one }); let tg = b.finish(vec![out])?; - Ok(Lowered { roles: vec![("T", tg), ("g_s", net_graph(&gn, true)?)] }) + Ok(Lowered { roles: vec![("T", tg), ("g_s", net_graph(gn, true)?)] }) } diff --git a/wai/wai-rs/src/int_audio.rs b/wai/wai-rs/src/int_audio.rs index 402eee812..c0d254683 100644 --- a/wai/wai-rs/src/int_audio.rs +++ b/wai/wai-rs/src/int_audio.rs @@ -518,9 +518,7 @@ pub mod ops { pub fn load(m: &AudioModel) -> Result { const WHAT: &str = "the synthesis does not lower to an integer-operator graph that loads under acc64"; let net = wam1_net(m).map_err(|r| kind(r, WHAT))?; - let g = wai_ops::lower::wam1_net(&net).map_err(|r| kind(r, WHAT))?; - drop(net); - crate::int_ops::load(&g).map_err(|r| kind(r, WHAT)) + wai_ops::lower::wam1_net_loaded(net, crate::int_ops::PROFILE).map_err(|r| kind(r, WHAT)) } /// Everything the decode checks before it decodes a symbol: [`audio_plan`], then the diff --git a/wai/wai-rs/src/int_hyper_synth.rs b/wai/wai-rs/src/int_hyper_synth.rs index 2f978d548..2bbb0da61 100644 --- a/wai/wai-rs/src/int_hyper_synth.rs +++ b/wai/wai-rs/src/int_hyper_synth.rs @@ -1326,11 +1326,19 @@ pub mod ops { pub g_s: Loaded, } + /// One network lowered as [`lowered`] lowers it, then loaded under `acc64` + /// ([`wai_ops::lower::whm1_net_loaded`]: the result of loading the lowered graph's + /// bytes, without writing and parsing them a second time). + fn loaded(m: &IntModel, rgb8: bool, what: &'static str) -> Result { + let net = crate::int_ops::whm1_net(m).map_err(|r| kind(r, what))?; + wai_ops::lower::whm1_net_loaded(net, rgb8, crate::int_ops::PROFILE).map_err(|r| kind(r, what)) + } + /// `h_s`, then `g_s`, each lowered and loaded under `acc64` in turn, so that no more /// than one network's lowering is held at once. pub fn load(g_s: &IntModel, h_s: &IntModel) -> Result { - let h = crate::int_ops::load(&lowered(h_s, false, H_S)?).map_err(|r| kind(r, H_S))?; - let g = crate::int_ops::load(&lowered(g_s, true, G_S)?).map_err(|r| kind(r, G_S))?; + let h = loaded(h_s, false, H_S)?; + let g = loaded(g_s, true, G_S)?; Ok(Nets { h_s: h, g_s: g }) } diff --git a/wai/wai-rs/src/int_splat.rs b/wai/wai-rs/src/int_splat.rs index 403b2b217..9ce3399b3 100644 --- a/wai/wai-rs/src/int_splat.rs +++ b/wai/wai-rs/src/int_splat.rs @@ -224,9 +224,7 @@ pub mod ops { pub fn load(gs: &IntModel) -> Result { const WHAT: &str = "g_s does not lower to an integer-operator graph that loads under acc64"; let net = crate::int_ops::whm1_net(gs).map_err(|r| kind(r, WHAT))?; - let g = wai_ops::lower::whm1_net(&net, true).map_err(|r| kind(r, WHAT))?; - drop(net); - crate::int_ops::load(&g).map_err(|r| kind(r, WHAT)) + wai_ops::lower::whm1_net_loaded(net, true, crate::int_ops::PROFILE).map_err(|r| kind(r, WHAT)) } /// Everything the decode checks before it decodes a symbol: [`splat_plan`], then `g_s` -- 2.54.0 (Apple Git-157) From 365a3dfec9ad2e9ebaca0af9bb9bcdf422853191 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:40:56 -0400 Subject: [PATCH 05/22] wai-rs: the random-set differential holds load step 8 to its oracle Every graph the int_synth, splat, audio and int_hyper generators lower is passed by wai_ops::bounds and by wai_ops::bounds_oracle (a dev-dependency with the oracle feature): equal passes or the same refusal, and the same first failure under each profile; every verdict is reached. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/Cargo.toml | 3 + wai/wai-rs/tests/int_ops_differential.rs | 76 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index fe60fd11c..cbf1bedf8 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -862,6 +862,9 @@ required-features = ["key_discovery"] [dev-dependencies] serde_json = { version = "1", features = ["preserve_order"] } # construct manifests in tests +# The bounds pass as it stood before its closed forms (`wai_ops::bounds_oracle`), which +# the random-set differential (tests/int_ops_differential.rs) holds load step 8 to. +wai-ops = { path = "../wai-ops", features = ["oracle"] } [build-dependencies] pkg-config = "0.3" # locate system libflac (cross-platform; replaces diff --git a/wai/wai-rs/tests/int_ops_differential.rs b/wai/wai-rs/tests/int_ops_differential.rs index 2fdec1989..16ee75dc3 100644 --- a/wai/wai-rs/tests/int_ops_differential.rs +++ b/wai/wai-rs/tests/int_ops_differential.rs @@ -10,6 +10,10 @@ //! (`whm1/1`), `wai.audio.int_codec` (`wam1/1`) and `wai.neural.int_hyper` (`whm1/1`, its //! two networks; the keyframe of `wai.video.int_hyper` is the same decode). //! +//! The same generators hold load step 8 to its oracle: the bounds pass, whose +//! convolutions and inverse GDNs are in closed form, against the pass as it stood before +//! (`wai_ops::bounds_oracle`, every term folded in 512 bits), on every graph they lower. +//! //! The random cases run for a fixed count by default. `WAI_DIFF_SECS` runs each random //! test for that many seconds instead (the nightly run, `wai-ops-differential.yml`), and //! `WAI_DIFF_SEED` sets the first seed; a failing case names its seed. @@ -1363,3 +1367,75 @@ fn int_hyper_planned_breaks_are_new_refusals() { let tight = Whm::ConvT { inc: 1, out: 3, k: 1, s: 1, p: 0, op: 0, w: vec![1 << 11; 3], m: vec![1 << 40; 3], b: vec![0; 3] }; expect("a check range its producer cannot meet", &whm1_bytes(12, 16, 0, &[first, tight]), &h0, &cl, Some(RefusalKind::FieldRange)); } + +// ------------------------------------------------------------------ load step 8 + +/// What the bounds pass made of the graphs compared: refused, or the profiles met. +#[derive(Default, Debug)] +struct Verdicts { + graphs: u64, + refused: u64, + acc32: u64, + acc64_only: u64, + neither: u64, +} + +/// Load step 8's pass on `g` against its oracle: equal passes, node for node (every +/// intermediate, result, saturation mark, parameter condition, `K_c`, width and flag, +/// and every tensor's interval), or the same refusal; and the same first failure under +/// each profile. +fn same_pass(g: &wai_ops::Graph, v: &mut Verdicts, why: &dyn Fn() -> String) { + use wai_ops::bounds::{self, Profile}; + let sizes = wai_ops::plan::load_plan(g).unwrap_or_else(|r| panic!("a lowered graph plans: {r:?}\n{}", why())); + let (new, old) = (bounds::pass(g, sizes.as_deref()), wai_ops::bounds_oracle::pass(g, sizes.as_deref())); + assert!(new == old, "the bounds pass and its oracle differ\n{}", why()); + v.graphs += 1; + match (&new, &old) { + (Ok(a), Ok(b)) => { + for p in Profile::ALL { + assert_eq!(a.first_failure(g, p), b.first_failure(g, p), "{}", why()); + } + match (a.meets(g, Profile::Acc32), a.meets(g, Profile::Acc64)) { + (true, _) => v.acc32 += 1, + (false, true) => v.acc64_only += 1, + _ => v.neither += 1, + } + } + _ => v.refused += 1, + } +} + +/// The bounds pass against its oracle on every graph the four generators above lower: +/// `int_synth/1`'s graph, the splat set's `g_s`, the audio synthesis, and the +/// conditional codec's `h_s` and `g_s`. Every verdict is reached: a refusal by the pass +/// itself (a `check` range its producer cannot meet), `acc32`, `acc64` only, and neither. +#[test] +fn the_bounds_pass_equals_its_oracle_on_random_sets() { + let mut v = Verdicts::default(); + let n = cases("bounds pass", 3000, |r, seed| { + let why = || format!("seed {seed}"); + let (set, _) = int_synth_case(r); + if let Ok(l) = wai_ops::lower::int_synth(&set) { + for (_, g) in &l.roles { + same_pass(g, &mut v, &why); + } + } + let (set, _) = int_splat_case(r); + if let Ok(g) = wai_ops::lower::whm1(&set, true) { + same_pass(&g, &mut v, &why); + } + let (set, _) = int_audio_case(r); + if let Ok(g) = wai_ops::lower::wam1(&set) { + same_pass(&g, &mut v, &why); + } + let (g_s, h_s, _) = int_hyper_case(r); + for (set, rgb8) in [(&h_s, false), (&g_s, true)] { + if let Ok(g) = wai_ops::lower::whm1(set, rgb8) { + same_pass(&g, &mut v, &why); + } + } + }); + eprintln!("bounds pass: {n} cases, {v:?}"); + assert!(v.graphs > n * 2, "the generators must mostly lower: {} graphs from {n} cases", v.graphs); + assert!(n < 1000 || (v.refused > 0 && v.acc32 > 0 && v.acc64_only > 0 && v.neither > 0), "a verdict was not reached: {v:?}"); +} -- 2.54.0 (Apple Git-157) From 5dba411d2d790ac3465ac3d670713a3db4910c83 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:48:49 -0400 Subject: [PATCH 06/22] wai-ops: i64 sums where a bound shows they fit, one scan for a node's weight range, byte stores in the writer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The closed forms accumulate a tap set's sums in i64 when n·max|w| ≤ 2^63 − 1 (every partial sum then lies in i64), and in i128 otherwise. The pass finds a node's weight range once, for w_bits and for the acc32 width condition (a list fits i32 exactly when its least and greatest do). The WHG1 writer adds an integer list's bytes at once and stores each byte, where a copy of a few bytes is a call on wasm32. Co-Authored-By: Claude Opus 5.5 --- wai/wai-ops/src/bounds.rs | 97 +++++++++++++++++++++++++++------------ wai/wai-ops/src/graph.rs | 35 ++++++++++---- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/wai/wai-ops/src/bounds.rs b/wai/wai-ops/src/bounds.rs index e68028498..b17dbeab6 100644 --- a/wai/wai-ops/src/bounds.rs +++ b/wai/wai-ops/src/bounds.rs @@ -308,12 +308,36 @@ struct Sums { neg: i128, } -impl Sums { - #[inline] - fn add(&mut self, w: i64) { - self.pos += w.max(0) as i128; - self.neg += w.min(0) as i128; +/// An accumulator for [`Sums`]: `i64` where [`sums_fit_i64`] shows it suffices, `i128` +/// otherwise. +trait Acc: Copy + Default + core::ops::AddAssign + From + Into {} +impl Acc for i64 {} +impl Acc for i128 {} + +/// Whether every partial sum of at most `n` weights in `[mn, mx]` lies in `i64`: the +/// positive parts sum into `[0, n·B]` and the negative parts into `[−n·B, 0]`, with +/// `B = max(|mn|, |mx|)`, and `n·B ≤ 2^63 − 1` puts both inside `i64`. (`n < 2^64` and +/// `B ≤ 2^63`, so `n·B` is exact in `u128`.) +fn sums_fit_i64(n: usize, mn: i64, mx: i64) -> bool { + (n as u128) * (mn.unsigned_abs().max(mx.unsigned_abs()) as u128) <= i64::MAX as u128 +} + +/// The least and greatest of `v`, or `None` for an empty list. +fn range(v: &[i64]) -> Option<(i64, i64)> { + if v.is_empty() { + return None; } + Some(v.iter().fold((i64::MAX, i64::MIN), |(a, b), &x| (a.min(x), b.max(x)))) +} + +/// [`Sums`] of `ws`, accumulated in `A`. +fn sums(ws: &[i64]) -> Sums { + let (mut p, mut n) = (A::default(), A::default()); + for &w in ws { + p += A::from(w.max(0)); + n += A::from(w.min(0)); + } + Sums { pos: p.into(), neg: n.into() } } /// `A(K, b)` (§8.2) of a tap set `K` whose weights sum to `s`, over `[l, h]` with @@ -353,15 +377,8 @@ fn term_closed(wmin: i64, wmax: i64, l: i128, h: i128) -> Iv { /// [`taps`] in closed form, for a tap set `ws` over `[l, h]` (see [`narrow`]): the term /// hull and `A(K, b)`. `None` for an empty tap set, or where the fold saturates. fn taps_closed(ws: &[i64], l: i128, h: i128, b: i64) -> Option<(Iv, Iv)> { - if ws.is_empty() { - return None; - } - let (mut mn, mut mx, mut s) = (i64::MAX, i64::MIN, Sums::default()); - for &w in ws { - mn = mn.min(w); - mx = mx.max(w); - s.add(w); - } + let (mn, mx) = range(ws)?; + let s = if sums_fit_i64(ws.len(), mn, mx) { sums::(ws) } else { sums::(ws) }; Some((term_closed(mn, mx, l, h), acc_closed(s, l, h, b)?)) } @@ -385,20 +402,33 @@ fn convt_sums(w: &[i64], cin: usize, cout: usize, kh: usize, kw: usize, sh: usiz } // Tap (ii, jj) of a kernel is its residue (ii mod sh, jj mod sw)'s. let residue: Vec = (0..kk).map(|e| ((e / kw) % sh) * rw + (e % kw) % sw).collect(); + // A residue's tap set holds at most a channel's `cin · kh · kw` taps. + let (mn, mx) = range(w)?; + Some(if sums_fit_i64(cin * kk, mn, mx) { + convt_fold::(w, cout, kk, nb, &residue) + } else { + convt_fold::(w, cout, kk, nb, &residue) + }) +} + +/// [`ConvTSums`] from the weights in their order, each residue's sums accumulated in `A`. +fn convt_fold(w: &[i64], cout: usize, kk: usize, nb: usize, residue: &[usize]) -> ConvTSums { let (mut min, mut max) = (vec![i64::MAX; cout], vec![i64::MIN; cout]); - let mut sums = vec![Sums::default(); cout * nb]; + let mut acc = vec![(A::default(), A::default()); cout * nb]; for (k, blk) in w.chunks_exact(kk).enumerate() { let o = k % cout; let (mut mn, mut mx) = (min[o], max[o]); - let so = &mut sums[o * nb..(o + 1) * nb]; - for (&v, &r) in blk.iter().zip(&residue) { + let so = &mut acc[o * nb..(o + 1) * nb]; + for (&v, &r) in blk.iter().zip(residue) { mn = mn.min(v); mx = mx.max(v); - so[r].add(v); + so[r].0 += A::from(v.max(0)); + so[r].1 += A::from(v.min(0)); } (min[o], max[o]) = (mn, mx); } - Some(ConvTSums { min, max, sums }) + let sums = acc.into_iter().map(|(p, n)| Sums { pos: p.into(), neg: n.into() }).collect(); + ConvTSums { min, max, sums } } /// `conv_transpose`'s output channel `o` tap by tap, as §8.3 writes the rule: the hull of @@ -842,7 +872,11 @@ fn fits32(v: i64) -> bool { } /// The parameter conditions of §7.2 a node fails, in that table's order. -fn params32(op: &Op) -> Vec<&'static str> { +/// `wr` is the range of the node's weights (`conv`, `conv_transpose`) or `M` +/// (`chscale`), `None` when the list is empty: a list fits `i32` exactly when its least and +/// greatest values do. +fn params32(op: &Op, wr: Option<(i64, i64)>) -> Vec<&'static str> { + let fits = wr.is_none_or(|(mn, mx)| fits32(mn) && fits32(mx)); let mut v = Vec::new(); let rq_keys = |v: &mut Vec<&'static str>, rq: &Rq| { if !rq.mult.iter().all(|&m| fits32(m)) { @@ -856,15 +890,15 @@ fn params32(op: &Op) -> Vec<&'static str> { } }; match &op.p { - Params::Conv { rq, w, .. } | Params::ConvT { rq, w, .. } => { + Params::Conv { rq, .. } | Params::ConvT { rq, .. } => { rq_keys(&mut v, rq); - if !w.iter().all(|&x| fits32(x)) { + if !fits { v.push("weights"); } } Params::Rescale(rq) | Params::Mul(rq) | Params::Gram { rq, .. } | Params::Apply { rq, .. } => rq_keys(&mut v, rq), - Params::Chscale { shift, m, .. } => { - if !m.iter().all(|&x| fits32(x)) { + Params::Chscale { shift, .. } => { + if !fits { v.push("chscale.m"); } if *shift > 63 { @@ -1186,13 +1220,16 @@ pub fn pass_with(g: &Graph, sizes: Option<&[(u64, u64)]>, over: &[(u32, Iv)]) -> CONV | CONV_TRANSPOSE | CHSCALE | MUL | GRAM | APPLY => Some(ins.iter().map(Iv::bits).collect()), _ => None, }; - let w_bits = match &op.p { - Params::Conv { w, .. } | Params::ConvT { w, .. } | Params::Chscale { m: w, .. } => { - let (mn, mx) = (w.iter().copied().min().unwrap_or(0), w.iter().copied().max().unwrap_or(0)); - Some(Iv::of_i64(mn, mx).bits()) - } + // The weights' (or `M`'s) least and greatest, found once for `w_bits` and for + // the parameter condition on their width. + let wr = match &op.p { + Params::Conv { w, .. } | Params::ConvT { w, .. } | Params::Chscale { m: w, .. } => Some(range(w)), _ => None, }; + let w_bits = wr.map(|r| { + let (mn, mx) = r.unwrap_or((0, 0)); + Iv::of_i64(mn, mx).bits() + }); nodes.push(Node { index: k as u32, code: op.code, @@ -1200,7 +1237,7 @@ pub fn pass_with(g: &Graph, sizes: Option<&[(u64, u64)]>, over: &[(u32, Iv)]) -> inter: e.inter, results: e.results, outs, - params32: params32(op), + params32: params32(op, wr.flatten()), k_c: term.map(|t| k_c(&t)), a_bits, w_bits, diff --git a/wai/wai-ops/src/graph.rs b/wai/wai-ops/src/graph.rs index b50d17ddb..1361e81ff 100644 --- a/wai/wai-ops/src/graph.rs +++ b/wai/wai-ops/src/graph.rs @@ -902,16 +902,35 @@ impl Graph { } } -/// Each of `v` as its low `w` bytes, little-endian. The usual widths are written a value -/// at a time as the unsigned integer of that width (its low bytes), after one reservation. +/// Each of `v` as its low `w` bytes, little-endian. For the usual widths the list's bytes +/// are added at once and each value's bytes stored into them, one store a byte (a copy +/// of a few bytes is a call on some targets). fn put_ints(o: &mut Vec, v: &[i64], w: u8) { - o.reserve(v.len().saturating_mul(w as usize)); + let w = w as usize; + if !matches!(w, 1 | 2 | 4 | 8) { + v.iter().for_each(|&x| o.extend_from_slice(&x.to_le_bytes()[..w])); + return; + } + let start = o.len(); + o.resize(start + v.len() * w, 0); + let out = &mut o[start..]; match w { - 1 => o.extend(v.iter().map(|&x| x as u8)), - 2 => v.iter().for_each(|&x| o.extend_from_slice(&(x as u16).to_le_bytes())), - 4 => v.iter().for_each(|&x| o.extend_from_slice(&(x as u32).to_le_bytes())), - 8 => v.iter().for_each(|&x| o.extend_from_slice(&x.to_le_bytes())), - _ => v.iter().for_each(|&x| o.extend_from_slice(&x.to_le_bytes()[..w as usize])), + 1 => out.iter_mut().zip(v).for_each(|(d, &x)| *d = x as u8), + 2 => out.chunks_exact_mut(2).zip(v).for_each(|(d, &x)| { + d[0] = x as u8; + d[1] = (x >> 8) as u8; + }), + 4 => out.chunks_exact_mut(4).zip(v).for_each(|(d, &x)| { + d[0] = x as u8; + d[1] = (x >> 8) as u8; + d[2] = (x >> 16) as u8; + d[3] = (x >> 24) as u8; + }), + _ => out.chunks_exact_mut(8).zip(v).for_each(|(d, &x)| { + for (k, b) in d.iter_mut().enumerate() { + *b = (x >> (8 * k)) as u8; + } + }), } } -- 2.54.0 (Apple Git-157) From 66942062a138a98d3c2c5a0006a1f55a898037fa Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:50:06 -0400 Subject: [PATCH 07/22] wai-ops: the writer stores each value as one array; test tidying Co-Authored-By: Claude Opus 5.5 --- wai/wai-ops/src/bounds_oracle.rs | 4 ++-- wai/wai-ops/src/graph.rs | 22 +++++----------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/wai/wai-ops/src/bounds_oracle.rs b/wai/wai-ops/src/bounds_oracle.rs index c3fa249b3..84b94055a 100644 --- a/wai/wai-ops/src/bounds_oracle.rs +++ b/wai/wai-ops/src/bounds_oracle.rs @@ -950,7 +950,7 @@ mod tests { 1 => vec![weight(r)], _ => (0..c).map(|_| weight(r)).collect(), }; - let mut shift = |r: &mut Rng| r.pick(&[0u8, 1, 7, 16, 31, 32, 63, 64, 100, 127]); + let shift = |r: &mut Rng| r.pick(&[0u8, 1, 7, 16, 31, 32, 63, 64, 100, 127]); let shift: Vec = if s_kind == 1 { vec![shift(r)] } else { (0..c).map(|_| shift(r)).collect() }; let bias = if bias_kind == BiasKind::None { vec![] } else { (0..c).map(|_| weight(r)).collect() }; Rq { round, bias_kind, m_kind, s_kind, m_bytes: if m_kind == 0 { 0 } else { 8 }, b_bytes: if bias_kind == BiasKind::None { 0 } else { 8 }, mult, shift, bias } @@ -962,7 +962,7 @@ mod tests { fn op_params(r: &mut Rng, cin: u32) -> (u8, Params, u32) { match r.below(3) { 0 => { - let divisors: Vec = (1..=cin).filter(|d| cin % d == 0).collect(); + let divisors: Vec = (1..=cin).filter(|d| cin.is_multiple_of(*d)).collect(); let groups = r.pick(&divisors); let cout = groups * (1 + r.below(3) as u32); let (kh, kw) = (1 + r.below(4) as u32, 1 + r.below(4) as u32); diff --git a/wai/wai-ops/src/graph.rs b/wai/wai-ops/src/graph.rs index 1361e81ff..1f636c7cf 100644 --- a/wai/wai-ops/src/graph.rs +++ b/wai/wai-ops/src/graph.rs @@ -903,8 +903,8 @@ impl Graph { } /// Each of `v` as its low `w` bytes, little-endian. For the usual widths the list's bytes -/// are added at once and each value's bytes stored into them, one store a byte (a copy -/// of a few bytes is a call on some targets). +/// are added at once and each value's bytes stored into them as one array (a copy of a +/// few bytes through a slice is a call on some targets). fn put_ints(o: &mut Vec, v: &[i64], w: u8) { let w = w as usize; if !matches!(w, 1 | 2 | 4 | 8) { @@ -916,21 +916,9 @@ fn put_ints(o: &mut Vec, v: &[i64], w: u8) { let out = &mut o[start..]; match w { 1 => out.iter_mut().zip(v).for_each(|(d, &x)| *d = x as u8), - 2 => out.chunks_exact_mut(2).zip(v).for_each(|(d, &x)| { - d[0] = x as u8; - d[1] = (x >> 8) as u8; - }), - 4 => out.chunks_exact_mut(4).zip(v).for_each(|(d, &x)| { - d[0] = x as u8; - d[1] = (x >> 8) as u8; - d[2] = (x >> 16) as u8; - d[3] = (x >> 24) as u8; - }), - _ => out.chunks_exact_mut(8).zip(v).for_each(|(d, &x)| { - for (k, b) in d.iter_mut().enumerate() { - *b = (x >> (8 * k)) as u8; - } - }), + 2 => out.as_chunks_mut::<2>().0.iter_mut().zip(v).for_each(|(d, &x)| *d = (x as u16).to_le_bytes()), + 4 => out.as_chunks_mut::<4>().0.iter_mut().zip(v).for_each(|(d, &x)| *d = (x as u32).to_le_bytes()), + _ => out.as_chunks_mut::<8>().0.iter_mut().zip(v).for_each(|(d, &x)| *d = x.to_le_bytes()), } } -- 2.54.0 (Apple Git-157) From f56ffbc8f481e10ae73b851ee9b10cb206f5fef3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:53:08 -0400 Subject: [PATCH 08/22] wai-rs: SHA-256 through the Arm SHA-2 instructions on 64-bit Arm (sha2 asm, not Windows, not wasm32) Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/Cargo.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index cbf1bedf8..7f2476e9d 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -860,6 +860,15 @@ name = "wai_keys_cli" path = "tests/wai_keys_cli.rs" required-features = ["key_discovery"] +# On 64-bit Arm, SHA-256 through the processor's SHA-2 instructions where it has them +# (sha2's `asm` feature: its aarch64 backend, which detects the instructions at run time +# and falls back to the portable code). Hashing a parameter set is a decode's to identify +# it (SPEC §3.1), and a set can be tens of megabytes. The digests are the same bytes. +# Not on Windows, whose toolchain does not assemble the sha2-asm sources the feature +# brings in, and not on wasm32 or any other target, whose build this leaves as it is. +[target.'cfg(all(target_arch = "aarch64", not(target_os = "windows")))'.dependencies] +sha2 = { version = "0.10", features = ["asm"] } + [dev-dependencies] serde_json = { version = "1", features = ["preserve_order"] } # construct manifests in tests # The bounds pass as it stood before its closed forms (`wai_ops::bounds_oracle`), which -- 2.54.0 (Apple Git-157) From a95c371688198e54e457d20ca3514955962d1505 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:53:26 -0400 Subject: [PATCH 09/22] wai: lockfiles of the seven workspaces that build wai-rs take sha2-asm 0.6.4 cargo update --workspace in each: sha2-asm added, nothing else moves. Co-Authored-By: Claude Opus 5.5 --- wai/wai-pack/Cargo.lock | 10 ++++++++++ wai/wai-quantum-serve/Cargo.lock | 10 ++++++++++ wai/wai-relay/Cargo.lock | 10 ++++++++++ wai/wai-rs/Cargo.lock | 10 ++++++++++ wai/wai-rs/fuzz/Cargo.lock | 10 ++++++++++ wai/wai-spz-gen/Cargo.lock | 10 ++++++++++ wai/wai-web/Cargo.lock | 10 ++++++++++ 7 files changed, 70 insertions(+) diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index ba68033a7..67266f5ae 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -508,6 +508,16 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index cb7ca6cb4..b0e630f14 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -450,6 +450,16 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] diff --git a/wai/wai-relay/Cargo.lock b/wai/wai-relay/Cargo.lock index 846c6166d..57cc42520 100644 --- a/wai/wai-relay/Cargo.lock +++ b/wai/wai-relay/Cargo.lock @@ -789,6 +789,16 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index 35e758dfe..96929e34a 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -3664,6 +3664,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest 0.10.7", + "sha2-asm", ] [[package]] @@ -3677,6 +3678,15 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", +] + [[package]] name = "shlex" version = "2.0.1" diff --git a/wai/wai-rs/fuzz/Cargo.lock b/wai/wai-rs/fuzz/Cargo.lock index 8a9b903ae..23d27f629 100644 --- a/wai/wai-rs/fuzz/Cargo.lock +++ b/wai/wai-rs/fuzz/Cargo.lock @@ -1345,6 +1345,16 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] diff --git a/wai/wai-spz-gen/Cargo.lock b/wai/wai-spz-gen/Cargo.lock index eef2758a9..e65d009c3 100644 --- a/wai/wai-spz-gen/Cargo.lock +++ b/wai/wai-spz-gen/Cargo.lock @@ -740,6 +740,16 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index 33a654a8c..ceadfae8e 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -1021,6 +1021,16 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", ] [[package]] -- 2.54.0 (Apple Git-157) From a31c1fd3ababa22403b1e281d12141b6bba14074 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 12:59:14 -0400 Subject: [PATCH 10/22] =?UTF-8?q?wai:=20integer-operators=20=C2=A78.2=20in?= =?UTF-8?q?formative=20note=20on=20the=20closed=20form;=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §8.6's note no longer says the bounds pass can cost more than a decode: the lowering and the load read the whole set again, which is what a kept set saves. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 22 ++++++++++++++++++++++ wai/extensions/integer-operators.md | 10 +++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 0cd9b9c8a..23c5ff49c 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,28 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Load step 8 in closed form + +- **The bounds pass.** `wai_ops::bounds` now computes each `conv`'s and `conv_transpose`'s term hull and partial-sum interval ([integer-operators](extensions/integer-operators.md) §8.2) from a few sums per output channel, or per residue for `conv_transpose`: + - the least and greatest weight; + - the sums of the positive and of the negative weights. These are kept in `i64` where the tap count times the largest magnitude shows that every partial sum fits, and in `i128` otherwise. + + An `igdn` row now folds in `i128`. These forms give the rule's integers wherever no operation of its fold can saturate. The pass checks that condition: both input ends in `i128` with `l ≤ h`, the result within `±2^255`, and no `i128` overflow. Anywhere else it folds term by term in 512 bits, as before. §8.2 gains an informative note on the closed form. No certificate, verdict, refusal or output changes. +- **The oracle.** The pass as it stood is kept as `wai_ops::bounds_oracle`, test-only; another crate's tests reach it through the `oracle` feature. The two passes must agree node for node, with the same refusals and the same first failures, on: + - every committed lowered graph and graph vector; + - the pretrained conditional-codec set, when `WAI_SOTA_INT_DIR` names it; + - random operators over input intervals the pass never forms (the ends of `i64` and `i128`, up to `2^255`, and `l > h`); + - random graphs that reach saturation; + - every graph the integer-operator differential's generators lower (`tests/int_ops_differential.rs`). +- **The lowering and the load.** + - `Builder::finish` now returns the graph it parsed back from its bytes. + - `whm1_net_loaded` and `wam1_net_loaded` (new) load that graph without writing and parsing its bytes a second time (`graph::load_lowered`, crate-only). They give `load`'s result, refusal for refusal. The `int_hyper`, splat and audio drivers use them. + - The `WHM1` and `WAM1` lowerings now move each layer's lists into the graph instead of copying them. + - The `WHG1` writer now adds a list's bytes at once and stores each value as one array. + - The pass reads a node's weight range once. +- **SHA-256 on 64-bit Arm.** `wai-rs` now enables sha2's `asm` feature on `aarch64` targets other than Windows. The processor's SHA-2 instructions are used where it has them, detected at run time. The digests are the same, and the wasm32 build is byte-identical. The lockfiles of the workspaces that build `wai-rs` now include `sha2-asm`. +- **Cost.** For the pretrained conditional-codec set, lowering and loading both networks is now a small part of one decode, natively and in wasm32. A decode that loads its set now costs about what a decode that finds it held costs. The browser demo is one such decode: it decodes once per view. + ### Parameter sets kept loaded between decodes - **The store.** A decode through the integer operator set lowers its parameter set and diff --git a/wai/extensions/integer-operators.md b/wai/extensions/integer-operators.md index 710e56fe0..a7b2a2115 100644 --- a/wai/extensions/integer-operators.md +++ b/wai/extensions/integer-operators.md @@ -745,6 +745,14 @@ A(K, b) = [ min(0, b) + Σ_{k∈K} min(0, lo_k), max(0, b) + Σ_{k∈K} max(0, It contains every sum of every subset of `K`, with or without `b`. So it bounds every partial sum in every order and grouping (§6), the final value, and the final value at a position where padding removes taps. +*Informative: the same integers from a few sums.* Let every term of `K` be a weight's term interval over one input interval `[l, h]` with `l ≤ h`, as for `conv` and for each residue of `conv_transpose` (§8.3). Let `S⁺` and `S⁻` be the sums of the positive and of the negative weights, and `w_min` and `w_max` the least and the greatest weight. +- A weight `w > 0` has the term `[w·l, w·h]`, and adds `w·min(0, l)` to the lower end and `w·max(0, h)` to the upper end. A weight `w < 0` has the term `[w·h, w·l]`, and adds `w·max(0, h)` and `w·min(0, l)`. +- So `A(K, b) = [min(0, b) + min(0, l)·S⁺ + max(0, h)·S⁻, max(0, b) + max(0, h)·S⁺ + min(0, l)·S⁻]`. +- For a fixed end, `w·l` is monotone in `w`, so the hull of the term intervals is the corner hull of `[w_min, w_max]` and `[l, h]`. +- The left fold of §8.1 adds terms of one sign, so its partial sums lie between 0 and its result. When no term and neither end passes `2^255`, nothing saturates, and these are the pass's integers. Otherwise a certifier folds term by term. + +A certifier can therefore read each weight once and keep a few sums for each output channel, or for each residue of `conv_transpose`. The reference implementation does this. + ### 8.3 Operator bound rules The interval of each producer result and named intermediate is as follows. `[l, h]` is the input tensor's interval; `X = max(|l|, |h|)`. A rule that names a channel is applied per channel, and §8.1 hulls the results. @@ -902,7 +910,7 @@ Before a sink decodes through a parameter set, it MUST establish the set's verdi - A set whose verdict does not meet its capability's declared profile (§11) is refused (`model_mismatch`). - So is a set whose pin claims a profile that its verdict does not meet. -*Informative: what a sink may keep between decodes.* A sink may keep more than the verdict: the set's graphs as loaded, so that a later decode through the same set skips the lowering and the load, whose bounds pass can cost more than a decode for a large set. +*Informative: what a sink may keep between decodes.* A sink may keep more than the verdict: the set's graphs as loaded, so that a later decode through the same set skips the lowering and the load, which read the whole set again. - **The key** is what makes this sound. It is the SHA-256 of the exact bytes the set was lowered from (for a set of several members, of their listing, which is the digest a pin over exactly those members names, [SPEC](../SPEC.md) §3.1), together with the capability, the lowering and the profile the graphs were loaded under. A name, a path or a label does not fix the bytes: keyed by one, a set could decode with the graphs of another set of the same name. - **Only what loaded.** An entry is made after every load step of §10.5 has passed. A set that is refused is not kept, so it is lowered and loaded again on its next decode and refused again, with the same code. - **Nothing else moves.** A decode makes every check before the load as before, in the order its payload extension gives ([integer-payloads](integer-payloads.md) §7), and the graphs run as they do after a fresh load, so the output is the same bytes. -- 2.54.0 (Apple Git-157) From 950cf1c959ecdeff4f40883a9670921d22bf08d9 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 21:38:57 -0400 Subject: [PATCH 11/22] int_mlicpp gate 1: the bundle's lowering and certificates, as wai-rs loads them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit int_mlicpp::ops::{lower, load, Nets}: the three WHM1 networks of a WMB1 bundle lowered by whm1/1 (h_s, entropy, then g_s with the RGB8 step; wmb1/1, integer-operators §13.4) from the networks this crate parses (int_ops::whm1_net), and loaded under acc64 with whm1_net_loaded. tests/int_mlicpp_ops.rs: every committed set lowers to its committed graphs from its bytes and from the parsed networks, the certificate is those graphs', and the load gives its verdict (acc64, not acc32; first acc32 failure h_s's input range, 47 bits for the vector's set as Appendix C predicts, 54 for the synthetic one). Named in both fast-kernel steps of wai-conformance.yml. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 7 +- wai/wai-rs/Cargo.toml | 5 + wai/wai-rs/src/int_mlicpp.rs | 55 +++++++++ wai/wai-rs/tests/int_mlicpp_ops.rs | 157 ++++++++++++++++++++++++++ 4 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 wai/wai-rs/tests/int_mlicpp_ops.rs diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 7aa85d469..8f918ddee 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -255,7 +255,8 @@ jobs: # loads with its certificate's verdict. tests/int_audio_ops.rs does the same for # wai.audio.int_codec and its lowering wam1/1 (§13.3), and tests/int_hyper_ops.rs # for wai.neural.int_hyper and the keyframe of wai.video.int_hyper (whm1/1: h_s, - # then g_s with the RGB8 step). + # then g_s with the RGB8 step), and tests/int_mlicpp_ops.rs for wai.neural.int_mlicpp + # (wmb1/1: h_s, entropy, then g_s with the RGB8 step). # tests/int_mlicpp.rs decodes the committed wai.neural.int_mlicpp vector against # its pinned hashes (image, every scale bucket, every symbol) under every kernel # policy and thread count, and checks that damaged payloads are refused rather than @@ -319,10 +320,10 @@ jobs: # codestream. No step named any of them until scripts/check-tests-in-ci.py asked. - name: Fast integer kernels ≡ reference kernels (overflow checks on) working-directory: wai/wai-rs - run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_set_cache --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration + run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_mlicpp_ops --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_set_cache --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration - name: Fast integer kernels ≡ reference kernels (release, overflow checks off) working-directory: wai/wai-rs - run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_set_cache --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test manifest_parse_memory --test manifest_reader_equivalence --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration + run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_mlicpp_ops --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_set_cache --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test manifest_parse_memory --test manifest_reader_equivalence --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration # The integer decode path builds under each of its features alone, so a sink can take # one medium without the others, and for wasm32 (a browser sink); `int_payload` arms a # medium only under its feature, and none needs `neural`. So does the staged-measurement diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index b09230d84..cccef49b7 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -781,6 +781,11 @@ name = "int_mlicpp_preflight" path = "tests/int_mlicpp_preflight.rs" required-features = ["neural_int"] +[[test]] +name = "int_mlicpp_ops" +path = "tests/int_mlicpp_ops.rs" +required-features = ["legacy_int"] + [[test]] name = "cli_stage" path = "tests/cli_stage.rs" diff --git a/wai/wai-rs/src/int_mlicpp.rs b/wai/wai-rs/src/int_mlicpp.rs index dacc63a9b..eaea4b350 100644 --- a/wai/wai-rs/src/int_mlicpp.rs +++ b/wai/wai-rs/src/int_mlicpp.rs @@ -490,6 +490,61 @@ fn decode_inner(model: &IntMlicModel, payload: &[u8], mut trace: Option<&mut Tra Ok(IntMlicImage { rgb: to_rgb8(&x, 3, hd.height, hd.width, afrac), width: hd.width, height: hd.height }) } +/// The bundle's networks through the integer operator set +/// (`extensions/integer-operators.md`): `wmb1/1` (§13.4) lowers each of the three `WHM1` +/// networks by `whm1/1` (§13.2), `h_s` and `entropy` as they are and `g_s` with the RGB8 +/// step (§13.0; §13.0's roles, in that order), and each is loaded under `acc64` with its +/// bounds certificate ([`crate::int_ops`]). The tables section is the stage operators' +/// data, and is not lowered. +pub mod ops { + use super::*; + use wai_ops::{Graph, Loaded, Refusal}; + + /// A refusal of a network's lowering or load: the bundle does not fit (`Model`), with + /// the operator set's code and site. + fn not_loaded(what: &str, r: Refusal) -> IntMlicError { + model_err(format!("{what} does not lower to an integer-operator graph that loads under acc64 ({:?} at {:?})", r.code, r.at)) + } + + /// One network lowered by `whm1/1`, with the RGB8 step where `rgb8` is set, from the + /// model this crate parsed ([`crate::int_ops::whm1_net`]). + fn lowered(m: &IntModel, rgb8: bool, what: &str) -> Result { + let net = crate::int_ops::whm1_net(m).map_err(|r| not_loaded(what, r))?; + wai_ops::lower::whm1_net(&net, rgb8).map_err(|r| not_loaded(what, r)) + } + + /// The bundle's three graphs, `h_s`, `entropy`, then `g_s` (with the RGB8 step), as + /// `wmb1/1` writes them from its bytes ([`wai_ops::lower::wmb1`]). + pub fn lower(m: &IntMlicModel) -> Result<(Graph, Graph, Graph), IntMlicError> { + Ok((lowered(&m.h_s, false, "h_s")?, lowered(&m.entropy, false, "entropy")?, lowered(&m.g_s, true, "g_s")?)) + } + + /// The bundle's three networks, lowered and loaded: the set as the decode holds it, + /// beside the parsed model. + pub struct Nets { + pub h_s: Loaded, + pub entropy: Loaded, + pub g_s: Loaded, + } + + /// One network lowered as [`lowered`] lowers it, then loaded under `acc64` + /// ([`wai_ops::lower::whm1_net_loaded`]: the result of loading the lowered graph's + /// bytes, without writing and parsing them a second time). + fn loaded(m: &IntModel, rgb8: bool, what: &str) -> Result { + let net = crate::int_ops::whm1_net(m).map_err(|r| not_loaded(what, r))?; + wai_ops::lower::whm1_net_loaded(net, rgb8, crate::int_ops::PROFILE).map_err(|r| not_loaded(what, r)) + } + + /// `h_s`, `entropy`, then `g_s`, each lowered and loaded under `acc64` in turn, so that + /// no more than one network's lowering is held at once. + pub fn load(m: &IntMlicModel) -> Result { + let h_s = loaded(&m.h_s, false, "h_s")?; + let entropy = loaded(&m.entropy, false, "entropy")?; + let g_s = loaded(&m.g_s, true, "g_s")?; + Ok(Nets { h_s, entropy, g_s }) + } +} + /// A small synthetic model and payload, built in memory, so these tests need no files /// (the committed vector is decoded by `tests/int_mlicpp.rs` and the /// `byte-exact-conformance` golden). diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs new file mode 100644 index 000000000..f4206791d --- /dev/null +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -0,0 +1,157 @@ +//! `wai.neural.int_mlicpp` through the integer operator set: its lowering `wmb1/1` +//! (`extensions/integer-operators.md` §13.4; each of the bundle's three `WHM1` networks by +//! `whm1/1`, §13.2: `h_s`, `entropy`, then `g_s` with the RGB8 step of §13.0) and the +//! certificate of every committed set (§8.5), as this crate loads them (`int_ops`, +//! `int_mlicpp::ops`). +//! +//! The lowered graphs and certificates are committed under `ops-conformance/certificates/`, +//! where `wai-verify` and the independent checker (`tools/wai_ops_check.py`) each reproduce +//! them byte for byte (`wai-ops.yml`). Here the lowering this crate runs, from the networks +//! it parses (`int_ops::whm1_net`), must write those same bytes, and the load a sink makes +//! must give the verdict the certificate records. + +use std::path::PathBuf; + +use serde_json::Value; +use wai::int_hyper_synth::IntModel; +use wai::int_mlicpp::{self as mlic, IntMlicModel}; +use wai_ops::Profile; + +const CAP: &str = "wai.neural.int_mlicpp"; + +fn wai() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn read(rel: &str) -> Vec { + std::fs::read(wai().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}")) +} + +fn json(rel: &str) -> Value { + serde_json::from_slice(&read(rel)).unwrap_or_else(|e| panic!("{rel}: {e}")) +} + +fn sha256(b: &[u8]) -> String { + wai_ops::sha256::sha256(b).iter().map(|x| format!("{x:02x}")).collect() +} + +/// A `WMB1` bundle's four sections, in order: `h_s`, `entropy`, `g_s` and the tables. +fn sections(b: &[u8]) -> [Vec; 4] { + assert_eq!(&b[..4], b"WMB1"); + let mut o = 4; + let mut s: [Vec; 4] = Default::default(); + for x in &mut s { + let n = u32::from_le_bytes(b[o..o + 4].try_into().unwrap()) as usize; + *x = b[o + 4..o + 4 + n].to_vec(); + o += 4 + n; + } + assert_eq!(o, b.len(), "nothing follows the tables"); + s +} + +/// A committed set of the capability, as `ops-conformance/certificates/index.json` lists +/// it: its name, its one member's path below `wai/`, its digest and its lowered graphs +/// (`h_s`, `entropy`, then `g_s`). +struct Indexed { + name: String, + bundle: String, + digest: String, + roles: [String; 3], +} + +fn indexed() -> Vec { + json("ops-conformance/certificates/index.json")["sets"] + .as_array() + .unwrap() + .iter() + .filter(|s| s["capability"] == CAP) + .map(|s| { + assert_eq!(s["lowering"], "wmb1/1"); + let roles = s["roles"].as_array().unwrap(); + assert_eq!(roles.len(), 3, "wmb1/1 lowers a bundle to three graphs"); + let members = s["members"].as_object().unwrap(); + assert_eq!(members.keys().collect::>(), ["model.wmb"], "a one-file set"); + let role = |k: usize| roles[k].as_str().unwrap().to_string(); + Indexed { + name: s["name"].as_str().unwrap().to_string(), + bundle: members["model.wmb"].as_str().unwrap().to_string(), + digest: s["set_sha256"].as_str().unwrap().to_string(), + roles: [role(0), role(1), role(2)], + } + }) + .collect() +} + +/// Gate 1 of the migration: every committed set of `wai.neural.int_mlicpp` lowers to its +/// committed graphs, from its bundle's bytes (`lower::wmb1`) and from the networks this +/// crate parses (`int_mlicpp::ops::lower`), whose conversion (`int_ops::whm1_net`) gives the +/// networks `wai-ops` parses from each section; the certificate beside them is those +/// graphs'; and loading each graph under the capability's profile gives the certificate's +/// verdict. The sets carry `check` ranges (§13.2), so they meet `acc64` by construction and +/// not `acc32`; the first `acc32` failure is `h_s`'s input range, at 47 bits for the +/// conformance vector's set (Appendix C's prediction) and 54 for the synthetic one. The +/// decode's own load (`int_mlicpp::ops::load`) holds those graphs. +#[test] +fn every_committed_set_lowers_to_its_committed_graphs_and_verdict() { + let sets = indexed(); + let names: Vec<&str> = sets.iter().map(|s| s.name.as_str()).collect(); + assert_eq!(names, ["int_mlicpp", "mlicpp"], "the committed int_mlicpp sets"); + for s in &sets { + let name = &s.name; + let bundle = read(&s.bundle); + assert_eq!(wai_ops::generate::sets::set_digest(&[("model.wmb", bundle.clone())]), s.digest, "{name}: the set's digest"); + assert_eq!(sha256(&bundle), s.digest, "{name}: a one-file set's digest is its file's"); + // The tables are the stage operators' data, not lowered; the bundle must load. + let m = IntMlicModel::from_bundle(&bundle).unwrap_or_else(|e| panic!("{name}: {e}")); + let lowered = wai_ops::lower::wmb1(&bundle).unwrap_or_else(|r| panic!("{name}: does not lower: {r:?}")); + let roles: Vec<&str> = lowered.roles.iter().map(|(r, _)| *r).collect(); + assert_eq!(roles, ["h_s", "entropy", "g_s"], "{name}"); + // The conversion of each network this crate parses is the one wai-ops parses from + // its section. + let secs = sections(&bundle); + for (k, net) in [&m.h_s, &m.entropy, &m.g_s].into_iter().enumerate() { + let conv = wai::int_ops::whm1_net(net).expect("a network parsed from bytes converts"); + assert!(conv == wai_ops::lower::parse_whm1(&secs[k]).unwrap(), "{name} {}: the converted network is the one wai-ops parses from its section", roles[k]); + let again = wai::int_ops::whm1_net(&IntModel::from_bytes(&secs[k]).unwrap()).unwrap(); + assert!(conv == again, "{name} {}: the bundle's network is its section's", roles[k]); + } + let (h, e, g) = mlic::ops::lower(&m).unwrap_or_else(|r| panic!("{name}: {r}")); + let cert = json(&format!("ops-conformance/certificates/{CAP}/{}.json", s.digest)); + assert_eq!(cert["set_sha256"].as_str(), Some(s.digest.as_str()), "{name}"); + assert_eq!(cert["capability"].as_str(), Some(CAP), "{name}"); + assert_eq!(cert["lowering"], "wmb1/1", "{name}"); + for (k, (role, ours)) in [("h_s", &h), ("entropy", &e), ("g_s", &g)].into_iter().enumerate() { + let from_bytes = lowered.get(role).unwrap(); + // The verdict first: the load a sink makes. + let loaded = wai::int_ops::load(from_bytes).unwrap_or_else(|r| panic!("{name} {role}: refused under acc64: {r:?}")); + assert!(!loaded.meets(Profile::Acc32), "{name} {role}: a graph with check ranges inside it does not meet acc32"); + // Then the committed bytes: the graph, both ways, and the certificate's record of it. + let graph = from_bytes.to_bytes(); + assert!(graph == read(&s.roles[k]), "{name}: the lowering of the bytes differs from the committed {}", s.roles[k]); + assert!(ours.to_bytes() == graph, "{name}: the lowering of the parsed networks differs from the committed {role}"); + let c = &cert["graphs"][k]; + assert_eq!(c["name"].as_str(), Some(role), "{name}"); + assert_eq!(c["graph_sha256"].as_str(), Some(sha256(&graph).as_str()), "{name} {role}: the certificate is the graph's"); + // The input's check range is the lowering's X_0 for the network's first layer (§13.2). + let x0 = &c["inputs"][0]; + let t = &from_bytes.tensors[0]; + assert_eq!((x0["policy"].as_str(), x0["lo"].as_str(), x0["hi"].as_str()), (Some("check"), Some(t.lo.to_string().as_str()), Some(t.hi.to_string().as_str())), "{name} {role}"); + assert!(t.hi > 0 && t.lo == -t.hi, "{name} {role}: a symmetric check range"); + } + let v = &cert["verdict"]; + assert_eq!((v["acc64"].as_bool(), v["acc32"].as_bool()), (Some(true), Some(false)), "{name}: the certificate's verdict"); + let bits = if name == "int_mlicpp" { "47" } else { "54" }; + let first = &v["acc32_first_failure"]; + assert_eq!( + (first["graph"].as_str(), first["where"].as_str(), first["index"].as_str(), first["key"].as_str(), first["bits"].as_str()), + (Some("h_s"), Some("input"), Some("0"), Some("range"), Some(bits)), + "{name}: the first acc32 failure" + ); + // The decode's own load: the three networks, in that order. + let nets = mlic::ops::load(&m).unwrap_or_else(|r| panic!("{name}: {r}")); + assert!( + nets.h_s.graph() == lowered.get("h_s").unwrap() && nets.entropy.graph() == lowered.get("entropy").unwrap() && nets.g_s.graph() == lowered.get("g_s").unwrap(), + "{name}: the decode loads the committed graphs" + ); + } +} -- 2.54.0 (Apple Git-157) From 8dfaec1147b99d5710d9a4740a5f629abbffc7e6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 21:44:10 -0400 Subject: [PATCH 12/22] int_mlicpp gate 2: the bundle decodes through wmb1/1 to its goldens The decode's stage steps (int_mlicpp::check, hyper_latent, latent: the header and the networks' geometry, the z stream, the y stream's slices and passes with y 0 where not yet decoded, t2/lenient/final_only) are shared by two decodes: int_mlicpp::legacy, the direct decode (run_net_checked per network, to_rgb8), unchanged, and int_mlicpp::ops, through the integer operator set: check, then the three networks lowered and loaded (load), then each graph's run-time plan on the payload's shapes (fit), then decode_loaded (h_s's graph; the entropy graph before every pass, its output split into sigma and mu by the driver; g_s's graph with the RGB8 step). int_mlicpp::decode still calls the direct decode (the switch is gate 6). Goldens: tests/int_mlicpp_ops.rs, the committed vector (both envelopes) through the lowering equals the direct decode, bucket for bucket, symbol for symbol and byte for byte, and every digest in expected.json; byte-exact-conformance int_mlicpp_through_its_lowering_is_golden, under the reference and the fast kernels. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-byte-equality.yml | 8 +- .../src/int_mlicpp_golden.rs | 22 ++ wai/wai-rs/src/int_mlicpp.rs | 224 +++++++++++++++--- wai/wai-rs/tests/int_mlicpp_ops.rs | 53 +++++ 4 files changed, 273 insertions(+), 34 deletions(-) diff --git a/.github/workflows/wai-byte-equality.yml b/.github/workflows/wai-byte-equality.yml index fc89dbea6..4b42ac20a 100644 --- a/.github/workflows/wai-byte-equality.yml +++ b/.github/workflows/wai-byte-equality.yml @@ -75,8 +75,8 @@ on: # by default: the goldens here decode through them. - 'wai/wai-rs/src/int_kernels.rs' # The integer operator set and the bridge to it: the goldens decode int_synth, - # the audio and splat codecs and the conditional codec (WIH1, the WIV1 - # keyframe) through their lowerings too. + # the audio and splat codecs, the conditional codec (WIH1, the WIV1 keyframe) + # and the MLIC++-topology codec through their lowerings too. - 'wai/wai-rs/src/int_ops.rs' - 'wai/wai-ops/**' - 'wai/wai-rs/tests/vectors/int_video/**' @@ -113,8 +113,8 @@ on: # by default: the goldens here decode through them. - 'wai/wai-rs/src/int_kernels.rs' # The integer operator set and the bridge to it: the goldens decode int_synth, - # the audio and splat codecs and the conditional codec (WIH1, the WIV1 - # keyframe) through their lowerings too. + # the audio and splat codecs, the conditional codec (WIH1, the WIV1 keyframe) + # and the MLIC++-topology codec through their lowerings too. - 'wai/wai-rs/src/int_ops.rs' - 'wai/wai-ops/**' - 'wai/wai-rs/tests/vectors/int_video/**' diff --git a/wai/byte-exact-conformance/src/int_mlicpp_golden.rs b/wai/byte-exact-conformance/src/int_mlicpp_golden.rs index 51e5ca23c..f0cc04ba4 100644 --- a/wai/byte-exact-conformance/src/int_mlicpp_golden.rs +++ b/wai/byte-exact-conformance/src/int_mlicpp_golden.rs @@ -81,3 +81,25 @@ fn the_int_mlicpp_vector_decodes_identically() { assert_eq!(got[2].1, RGB_FNV1A, "the pixels differ: the synthesis diverged"); assert_eq!(got[3].1, RGB_SHA256); } + +/// The same vector through the integer operator set (`int_mlicpp::ops`: the bundle lowered +/// by `wmb1/1`, its three graphs loaded under `acc64` and run by the interpreter), under the +/// reference kernels and the fast ones: the same buckets, symbols and pixels. +#[test] +fn int_mlicpp_through_its_lowering_is_golden() { + use crate::int_kernels::{with_policy, Policy}; + let (bundle, env) = (read("model.wmb"), read("stream.wai")); + let pl = payload(&env); + let model = IntMlicModel::from_bundle(&bundle).expect("model bundle"); + for p in [Policy::reference(), Policy::fast(0)] { + let (trace, img) = with_policy(p, || crate::int_mlicpp::ops::decode_traced(&model, pl)); + let img = img.expect("decode"); + let buckets: Vec = trace.buckets.iter().flat_map(|&b| (b as u16).to_le_bytes()).collect(); + let symbols: Vec = trace.symbols.iter().flat_map(|s| s.to_le_bytes()).collect(); + assert_eq!((img.width, img.height), (64, 64), "{p:?}"); + assert_eq!(format!("{:016x}", fnv1a(&buckets)), BUCKET_FNV1A, "{p:?}: a bucket differs from the encoder's"); + assert_eq!(format!("{:016x}", fnv1a(&symbols)), SYMBOL_FNV1A, "{p:?}: a symbol differs under the encoder's buckets"); + assert_eq!(format!("{:016x}", fnv1a(&img.rgb)), RGB_FNV1A, "{p:?}: the pixels differ"); + assert_eq!(sha256(&img.rgb), RGB_SHA256, "{p:?}"); + } +} diff --git a/wai/wai-rs/src/int_mlicpp.rs b/wai/wai-rs/src/int_mlicpp.rs index eaea4b350..a2579da2a 100644 --- a/wai/wai-rs/src/int_mlicpp.rs +++ b/wai/wai-rs/src/int_mlicpp.rs @@ -379,7 +379,7 @@ pub fn decode_bytes(payload: &[u8], bundle: &[u8]) -> Result Result { - decode_inner(model, payload, None) + legacy::decode(model, payload) } /// [`decode`], also returning the `y` stream's [`Trace`] — as far as the decode got, @@ -388,9 +388,7 @@ pub fn decode(model: &IntMlicModel, payload: &[u8]) -> Result (Trace, Result) { - let mut trace = Trace::default(); - let img = decode_inner(model, payload, Some(&mut trace)); - (trace, img) + legacy::decode_traced(model, payload) } /// Check, from `net`'s layer geometry alone, that on a `[c, h, w]` input it gives @@ -409,22 +407,14 @@ fn preflight(net: &IntModel, what: &str, (c, h, w): (usize, usize, usize), want: Ok(()) } -/// Run `net` on `x` (`[c, h, w]`) through [`run_net_checked`], after [`preflight`]: -/// the payload is refused if its values could take a layer out of the exact integer -/// range, the model if a layer does not fit the payload's shapes. -fn run(net: &IntModel, what: &str, x: &[i64], (c, h, w): (usize, usize, usize), want: (usize, usize, usize)) -> Result, IntMlicError> { - preflight(net, what, (c, h, w), want)?; - let (out, oc, oh, ow) = run_net_checked(net, x, c, h, w).map_err(|why| { - let msg = format!("{what} refused, {why}"); - if why.out_of_range { payload_err(msg) } else { model_err(msg) } - })?; - debug_assert_eq!((oc, oh, ow), want, "net_shape and run_net_checked disagree on {what}'s output shape"); - Ok(out) -} - -fn decode_inner(model: &IntMlicModel, payload: &[u8], mut trace: Option<&mut Trace>) -> Result { +/// Everything the decode checks before it allocates anything sized from the header +/// (integer-payloads §14, step 1, as far as the networks' layer geometry gives it): the +/// header ([`parse_header`]); a `z` table for each of the hyper-latent's channels; then +/// `h_s`, `entropy` and `g_s` against the header's shapes ([`preflight`]). Returns the +/// header. +fn check(model: &IntMlicModel, payload: &[u8]) -> Result { let hd = parse_header(payload)?; - let (t, afrac) = (&model.tables, model.afrac); + let t = &model.tables; if hd.cz > t.z_cdfs.len() { return Err(model_err(format!("z has {} channels, the model's prior {}", hd.cz, t.z_cdfs.len()))); } @@ -434,10 +424,16 @@ fn decode_inner(model: &IntMlicModel, payload: &[u8], mut trace: Option<&mut Tra preflight(&model.h_s, "h_s", (hd.cz, hd.zh, hd.zw), (2 * cy, yh, yw))?; preflight(&model.entropy, "entropy", (3 * cy, yh, yw), (2 * cy, yh, yw))?; preflight(&model.g_s, "g_s", (cy, yh, yw), (3, hd.height, hd.width))?; - let (plane, zplane) = (yh * yw, hd.zh * hd.zw); - let n = cy * plane; + Ok(hd) +} - // 1. z: factorized, per channel. +/// Step 2 of the decode (integer-payloads §14): the hyper-latent `z`, per channel and in +/// raster order, each symbol decoded against its channel's table (`t2`, `lenient`) and +/// rebuilt as `s·2^afrac + median_q`. Under `final_only` the stream is tested once, after +/// the last symbol: a symbol decoded after it ran out is still rebuilt, and a value that +/// leaves `i64` refuses the payload before that test. +fn hyper_latent(t: &CondEntropy, afrac: u32, hd: &Header, payload: &[u8]) -> Result, IntMlicError> { + let zplane = hd.zh * hd.zw; let mut z = vec![0i64; hd.cz * zplane]; let mut dz = RansDecoder::new(&payload[hd.z.clone()]).map_err(|e| payload_err(format!("z stream: {e}")))?; for (ch, zc) in z.chunks_mut(zplane).enumerate() { @@ -449,18 +445,37 @@ fn decode_inner(model: &IntMlicModel, payload: &[u8], mut trace: Option<&mut Tra if dz.is_exhausted() { return Err(payload_err("z stream ended before its last symbol")); } + Ok(z) +} - // 2. hyper = h_s(z), then 3. the slices, each an anchor and a non-anchor pass. `ctx` - // is the entropy network's input, concat[y, hyper]; y is decoded into its first +/// Step 4 of the decode (integer-payloads §14): the latent `y`, given `hyper`, `h_s`'s +/// output. For each slice and each of its two passes (the anchors, then the rest), +/// `entropy` gives `σ‖μ` from the entropy network's input `concat(y, hyper)`, in which +/// every element of `y` not yet decoded is 0; then each of the slice's elements in the +/// pass, channel by channel in raster order, is decoded against the `ceil` bucket of its +/// `σ` and rebuilt as `r·2^afrac + μ`. One stream carries every pass, and under +/// `final_only` it is tested once, after the last symbol: symbols decoded after it ran out +/// are still rebuilt and fed to the passes after them, and a refusal those give (a value +/// that leaves `i64`, or one outside the entropy network's range) comes first. Returns the +/// entropy network's input as it stands at the end, whose first `Cy·Yh·Yw` elements are +/// `y`; the buckets and symbols go to `trace` as they are decoded. +#[allow(clippy::too_many_arguments)] +fn latent( + t: &CondEntropy, afrac: u32, hd: &Header, payload: &[u8], hyper: &[i64], + mut entropy: impl FnMut(&[i64]) -> Result, IntMlicError>, mut trace: Option<&mut Trace>, +) -> Result, IntMlicError> { + let (cy, yh, yw) = (hd.cy, hd.yh, hd.yw); + let plane = yh * yw; + let n = cy * plane; + // `ctx` is the entropy network's input, concat[y, hyper]; y is decoded into its first // `n` elements in place, and is zero wherever not yet decoded. - let hyper = run(&model.h_s, "h_s", &z, (hd.cz, hd.zh, hd.zw), (2 * cy, yh, yw))?; let mut ctx = vec![0i64; 3 * n]; - ctx[n..].copy_from_slice(&hyper); + ctx[n..].copy_from_slice(hyper); let mut dy = RansDecoder::new(&payload[hd.y.clone()]).map_err(|e| payload_err(format!("y stream: {e}")))?; let width = cy / hd.slices; for k in 0..hd.slices { for anchor in [true, false] { - let params = run(&model.entropy, "entropy", &ctx, (3 * cy, yh, yw), (2 * cy, yh, yw))?; + let params = entropy(&ctx)?; let (sigma, mu) = params.split_at(n); for ch in k * width..(k + 1) * width { for row in 0..yh { @@ -484,10 +499,56 @@ fn decode_inner(model: &IntMlicModel, payload: &[u8], mut trace: Option<&mut Tra if dy.is_exhausted() { return Err(payload_err("y stream ended before its last symbol")); } + Ok(ctx) +} - // 4. synthesis → RGB. - let x = run(&model.g_s, "g_s", &ctx[..n], (cy, yh, yw), (3, hd.height, hd.width))?; - Ok(IntMlicImage { rgb: to_rgb8(&x, 3, hd.height, hd.width, afrac), width: hd.width, height: hd.height }) +/// The direct decode, as `wai.neural.int_mlicpp` ran before it decoded through the +/// integer operator set: `h_s`, `entropy` and `g_s` run layer by layer by +/// [`run_net_checked`], which checks each layer's arithmetic on the values it is given +/// before it runs, then the RGB8 bytes formed here ([`to_rgb8`]). The stage steps +/// ([`check`], [`hyper_latent`], [`latent`]) are the decode's own. +pub mod legacy { + use super::*; + + /// Run `net` on `x` (`[c, h, w]`) through [`run_net_checked`], after [`preflight`]: + /// the payload is refused if its values could take a layer out of the exact integer + /// range, the model if a layer does not fit the payload's shapes. + pub(crate) fn run(net: &IntModel, what: &str, x: &[i64], (c, h, w): (usize, usize, usize), want: (usize, usize, usize)) -> Result, IntMlicError> { + preflight(net, what, (c, h, w), want)?; + let (out, oc, oh, ow) = run_net_checked(net, x, c, h, w).map_err(|why| { + let msg = format!("{what} refused, {why}"); + if why.out_of_range { payload_err(msg) } else { model_err(msg) } + })?; + debug_assert_eq!((oc, oh, ow), want, "net_shape and run_net_checked disagree on {what}'s output shape"); + Ok(out) + } + + /// The direct decode of [`super::decode`]'s arguments. + pub fn decode(model: &IntMlicModel, payload: &[u8]) -> Result { + decode_inner(model, payload, None) + } + + /// The direct decode of [`super::decode_traced`]'s arguments. + pub fn decode_traced(model: &IntMlicModel, payload: &[u8]) -> (Trace, Result) { + let mut trace = Trace::default(); + let img = decode_inner(model, payload, Some(&mut trace)); + (trace, img) + } + + fn decode_inner(model: &IntMlicModel, payload: &[u8], trace: Option<&mut Trace>) -> Result { + let hd = check(model, payload)?; + let (t, afrac) = (&model.tables, model.afrac); + let (cy, yh, yw) = (hd.cy, hd.yh, hd.yw); + let n = cy * yh * yw; + // 1. z, 2. hyper = h_s(z), then 3. the slices, each an anchor and a non-anchor pass. + let z = hyper_latent(t, afrac, &hd, payload)?; + let hyper = run(&model.h_s, "h_s", &z, (hd.cz, hd.zh, hd.zw), (2 * cy, yh, yw))?; + let entropy = |ctx: &[i64]| run(&model.entropy, "entropy", ctx, (3 * cy, yh, yw), (2 * cy, yh, yw)); + let ctx = latent(t, afrac, &hd, payload, &hyper, entropy, trace)?; + // 4. synthesis → RGB. + let x = run(&model.g_s, "g_s", &ctx[..n], (cy, yh, yw), (3, hd.height, hd.width))?; + Ok(IntMlicImage { rgb: to_rgb8(&x, 3, hd.height, hd.width, afrac), width: hd.width, height: hd.height }) + } } /// The bundle's networks through the integer operator set @@ -543,6 +604,108 @@ pub mod ops { let g_s = loaded(&m.g_s, true, "g_s")?; Ok(Nets { h_s, entropy, g_s }) } + + /// What the decode checks before it allocates anything sized from the header: the + /// header, a `z` table for each hyper-latent channel, and each network's layer geometry + /// against the header's shapes (the same checks, in the same order, as the direct + /// decode). Returns the header. + pub fn check(model: &IntMlicModel, payload: &[u8]) -> Result { + super::check(model, payload) + } + + /// The loaded graphs against the payload's shapes, before any stream + /// (integer-payloads §14, step 1): each graph's run-time plan + /// ([`wai_ops::plan::run_plan`], integer-operators §4) on its input, `[Cz, Zh, Zw]` for + /// `h_s`, `[3·Cy, Yh, Yw]` for `entropy` and `[Cy, Yh, Yw]` for `g_s`, must admit it (its + /// channel count, every size it forms within the caps, every shape rule), and give + /// `[2·Cy, Yh, Yw]`, `[2·Cy, Yh, Yw]` and `[3, H, W]`. A refusal is the bundle's: it does + /// not fit this payload. + pub fn fit(n: &Nets, hd: &Header) -> Result<(), IntMlicError> { + let (cy, yh, yw) = (hd.cy as u64, hd.yh as u64, hd.yw as u64); + let nets = [ + (&n.h_s, "h_s", (hd.cz as u64, hd.zh as u64, hd.zw as u64), (2 * cy, yh, yw)), + (&n.entropy, "entropy", (3 * cy, yh, yw), (2 * cy, yh, yw)), + (&n.g_s, "g_s", (cy, yh, yw), (3, hd.height as u64, hd.width as u64)), + ]; + for (l, what, input, want) in nets { + let g = l.graph(); + let sizes = wai_ops::plan::run_plan(g, &[input], &[]) + .map_err(|r| model_err(format!("{what} does not fit the payload's shapes ({:?} at {:?})", r.code, r.at)))?; + let out = g.outputs[0] as usize; + let (oh, ow) = sizes[out]; + if (g.tensors[out].channels as u64, oh, ow) != want { + return Err(model_err(format!("{what} gives [{}, {oh}, {ow}], the payload needs {want:?}", g.tensors[out].channels))); + } + } + Ok(()) + } + + /// A refusal of a graph as it runs: a value outside a `check` range is the payload's + /// (it is the direct decode's refusal of values a layer's arithmetic could overflow on); + /// any other is the bundle's. + fn ran(what: &str, r: Refusal) -> IntMlicError { + let msg = format!("{what} refused ({:?} at {:?})", r.code, r.at); + if r.code == wai_ops::Code::FieldRange { payload_err(msg) } else { model_err(msg) } + } + + /// [`super::decode`] through the integer operator set: [`check`], [`load`], [`fit`], + /// then [`decode_loaded`]. The same image, and the same refusals, except where + /// integer-operators §15.3 lists a change. + pub fn decode(model: &IntMlicModel, payload: &[u8]) -> Result { + decode_inner(model, payload, None) + } + + /// [`decode`], also returning the `y` stream's [`Trace`] as far as the decode got. + pub fn decode_traced(model: &IntMlicModel, payload: &[u8]) -> (Trace, Result) { + let mut trace = Trace::default(); + let img = decode_inner(model, payload, Some(&mut trace)); + (trace, img) + } + + fn decode_inner(model: &IntMlicModel, payload: &[u8], trace: Option<&mut Trace>) -> Result { + let hd = check(model, payload)?; + let n = load(model)?; + fit(&n, &hd)?; + decode_loaded(&n, &model.tables, model.afrac, &hd, payload, trace) + } + + /// The decode after its plan, from the bundle's loaded networks ([`load`]): `z`, then + /// `h_s`'s graph; then each pass of each slice, the entropy graph on `concat(y, hyper)` + /// (every element of `y` not yet decoded 0), whose output the driver splits into `σ` + /// (channels `[0, Cy)`) and `μ` (`[Cy, 2·Cy)`) (§13.5); then `g_s`'s graph, which ends + /// with the RGB8 step; then the bytes, row-major HWC. `hd` must be a header [`check`] + /// and [`fit`] admitted against that bundle, and `tables` and `afrac` the bundle's. + pub fn decode_loaded( + n: &Nets, tables: &CondEntropy, afrac: u32, hd: &Header, payload: &[u8], trace: Option<&mut Trace>, + ) -> Result { + use wai_ops::Tensor; + // The header bounded every size, so each fits u32. + let u = |v: usize| v as u32; + let (cy, yh, yw) = (hd.cy, hd.yh, hd.yw); + let nn = cy * yh * yw; + // 1. z, then h_s. + let z = hyper_latent(tables, afrac, hd, payload)?; + let hyper = crate::int_ops::run1(&n.h_s, Tensor::new(u(hd.cz), u(hd.zh), u(hd.zw), z), &[]).map_err(|r| ran("h_s", r))?; + // 2. The slices, each an anchor and a non-anchor pass, the entropy graph before each. + let entropy = |ctx: &[i64]| { + crate::int_ops::run1(&n.entropy, Tensor::new(u(3 * cy), u(yh), u(yw), ctx.to_vec()), &[]).map(|t| t.data).map_err(|r| ran("entropy", r)) + }; + let mut ctx = latent(tables, afrac, hd, payload, &hyper.data, entropy, trace)?; + drop(hyper); + // 3. g_s, ending with the RGB8 step, so every value is a byte; CHW → HWC. + ctx.truncate(nn); + let out = crate::int_ops::run1(&n.g_s, Tensor::new(u(cy), u(yh), u(yw), ctx), &[]).map_err(|r| ran("g_s", r))?; + let (h, w) = (out.h as usize, out.w as usize); + if (out.c, h, w) != (3, hd.height, hd.width) { + return Err(model_err("g_s does not give the image the payload declares")); + } + let plane = h * w; + let mut rgb = vec![0u8; 3 * plane]; + for (k, &v) in out.data.iter().enumerate() { + rgb[(k % plane) * 3 + k / plane] = v as u8; + } + Ok(IntMlicImage { rgb, width: w, height: h }) + } } /// A small synthetic model and payload, built in memory, so these tests need no files @@ -551,6 +714,7 @@ pub mod ops { #[cfg(test)] mod tests { use super::*; + use super::legacy::run; use crate::int_hyper_synth::IntLayer; use crate::int_kernels::testkit::{Rng, FAST}; use crate::int_kernels::{with_policy, Policy}; diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs index f4206791d..abd199206 100644 --- a/wai/wai-rs/tests/int_mlicpp_ops.rs +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -155,3 +155,56 @@ fn every_committed_set_lowers_to_its_committed_graphs_and_verdict() { ); } } + +fn hex(h: u64) -> String { + format!("{h:016x}") +} + +/// FNV-1a over the `y` stream's buckets (u16 LE) and symbols (i32 LE), in coding order. +fn trace_hashes(t: &mlic::Trace) -> (String, String) { + let b: Vec = t.buckets.iter().flat_map(|&b| u16::try_from(b).unwrap().to_le_bytes()).collect(); + let s: Vec = t.symbols.iter().flat_map(|s| s.to_le_bytes()).collect(); + (hex(wai::int_hyper_synth::fnv1a(&b)), hex(wai::int_hyper_synth::fnv1a(&s))) +} + +/// The conformance vector's directory, below `wai/`. +const DIR: &str = "wai-rs/tests/vectors/int_mlicpp"; + +/// The committed vector's bundle, parsed. +fn model() -> IntMlicModel { + IntMlicModel::from_bundle(&read(&format!("{DIR}/model.wmb"))).unwrap() +} + +/// The payload of a committed envelope of the vector. +fn payload_of(env: &str) -> Vec { + wai::container::Wai::from_bytes(&read(&format!("{DIR}/{env}"))).unwrap().payload +} + +/// Gate 2 of the migration: golden equality. The committed vector of +/// `wai.neural.int_mlicpp` (`tests/vectors/int_mlicpp`, its form-less envelope and its +/// set-form twin, which carry the same payload) decodes through the lowering +/// (`int_mlicpp::ops`) to the image the direct decode gives (`int_mlicpp::legacy`), with +/// the same bucket and symbol for every element, and to every digest `expected.json` +/// commits for it: the image's FNV-1a and SHA-256, the buckets' and the symbols' FNV-1a, +/// and the count of `y` symbols. +#[test] +fn the_committed_vector_decodes_through_the_lowering_to_its_golden() { + use sha2::Digest; + let exp = json(&format!("{DIR}/expected.json")); + let m = model(); + let (p, p_set) = (payload_of("stream.wai"), payload_of("stream_set.wai")); + assert_eq!(sha256(&p), exp["payload_sha256"].as_str().unwrap()); + assert!(p == p_set, "the set-form twin carries the same payload"); + let (t_ops, img_ops) = mlic::ops::decode_traced(&m, &p); + let (t_dir, img_dir) = mlic::legacy::decode_traced(&m, &p); + let (img_ops, img_dir) = (img_ops.expect("the decode through the lowering"), img_dir.expect("the direct decode")); + assert!(t_ops == t_dir, "every bucket and symbol, through the lowering and directly"); + assert!(img_ops == img_dir, "the decode through the lowering differs from the direct decode"); + assert_eq!((img_ops.width, img_ops.height), (exp["width"].as_u64().unwrap() as usize, exp["height"].as_u64().unwrap() as usize)); + assert_eq!(hex(wai::int_hyper_synth::fnv1a(&img_ops.rgb)), exp["rgb_fnv1a"].as_str().unwrap(), "the export's FNV-1a"); + let sha: String = sha2::Sha256::digest(&img_ops.rgb).iter().map(|x| format!("{x:02x}")).collect(); + assert_eq!(sha, exp["rgb_sha256"].as_str().unwrap(), "the image's SHA-256"); + let (b, s) = trace_hashes(&t_ops); + assert_eq!((b.as_str(), s.as_str()), (exp["bucket_fnv1a"].as_str().unwrap(), exp["symbol_fnv1a"].as_str().unwrap())); + assert_eq!(t_ops.buckets.len() as u64, exp["y_symbols"].as_u64().unwrap()); +} -- 2.54.0 (Apple Git-157) From f67f4818b521e261cc4f5ed95e5b9c7a987192be Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 21:51:55 -0400 Subject: [PATCH 13/22] int_mlicpp gate 3 (WIP: committed before running): refusal parity, the ops member, SPEC citations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/int_mlicpp_ops.rs: (b) 21 fixtures, one per refusal the direct decode makes in integer-payloads §14's order, each one change from the committed bundle or payload (the header; the bundle against it: z tables, each network's shape, the cap; each stream: too short, a value past i64, run out; each network's ranges); both decodes refuse each with the same code and read the same symbols before it; then the sink's codes for a payload that is not one (payload_malformed) and a bundle that is not one (model_mismatch); the end rule final_only: the y stream cut short by 4 to 64 bytes is decoded alike both ways; where it runs out, every symbol after is still decoded and fed to the passes after it, and the refusal comes at the end; the z stream likewise; (c) the edge of every check range of the committed h_s, entropy and g_s (ten layers; a ReLU reads none): at +-X_l both run alike, at +-(X_l + 1) both refuse. codecs::capability_ops: wai.neural.int_mlicpp = {acc64, full, wmb1/1, rans64 t2/lenient/final_only, bucket ceil}; the registry test lists six capabilities. SPEC §5's row cites integer-payloads §14 and integer-operators §13.4; SPEC §7 names it. CHANGELOG section started. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 11 ++ wai/SPEC.md | 5 +- wai/wai-rs/src/codecs/mod.rs | 4 + wai/wai-rs/src/codecs/registry.rs | 10 +- wai/wai-rs/tests/int_mlicpp_ops.rs | 251 +++++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 5 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index ca92bd4f2..1b4bb4ace 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,17 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### `wai.neural.int_mlicpp` through the integer operator set + +- **Its `ops` member in the registry** ([integer-operators](extensions/integer-operators.md) + §11). `registry/capabilities.json` gives `wai.neural.int_mlicpp` the member `{"set": + "wai-ops/1", "profile": "acc64", "subset": "full", "lowering": "wmb1/1", "stage": + {"rans64": {"table": "t2", "escape": "lenient", "end": "final_only"}, "bucket": + "ceil"}}`. SPEC §5's row cites the payload and bundle + ([integer-payloads](extensions/integer-payloads.md) §14) and the lowering + (integer-operators §13.4), and SPEC §7 names the capability among those that decode + through the operator set. + ### Load step 8 in closed form - **The bounds pass.** `wai_ops::bounds` now computes each `conv`'s and `conv_transpose`'s term hull and partial-sum interval ([integer-operators](extensions/integer-operators.md) §8.2) from a few sums per output channel, or per residue for `conv_transpose`: diff --git a/wai/SPEC.md b/wai/SPEC.md index 0d125da2c..583b91a8c 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -694,7 +694,7 @@ registration and makes no conforming envelope non-conforming (§8). | `wai.neural.mbt2018_mean` | mean-scale hyperprior (Gaussian-conditional entropy) | the conditional-entropy family (arXiv 1809.02736) | `neural/hyperprior.rs` | | `wai.neural.int_synth` | integer fixed-point synthesis (all-i64 transposed-conv, no float/ONNX) | bit-identical on every machine — soundly backs `intent = replicate` | payload and set: [integer-payloads](extensions/integer-payloads.md) §13; decoded through its lowering, [integer-operators](extensions/integer-operators.md) §13.1 (`int_synth/1`); `int_synth.rs` (integer weights sink-supplied; the reference tools make them by training in float, then quantising after training with per-layer shifts calibrated on measured activations, `tools/wai_int_synth_train.py`. Of the committed vectors only `photo` comes from a trained model, `tools/wai_int_synth_real.py`; `gradient`, `bloom`, `facade` and the cross-architecture golden use seeded integer weights) | | `wai.neural.int_hyper` | integer fixed-point **conditional** codec — whole bitstream→image path integer (integer hyper-decoder σ → integer-σ entropy → integer synthesis) | the integer-entropy contract fully assembled: a PTQ of mbt2018_mean, bit-identical on every machine, whose integer decode CI holds within 1 dB of the same model's float decode on a pinned photograph | payload `WIH1`; `int_hyper_synth.rs` (tables sink-supplied). Payload: [integer-payloads](extensions/integer-payloads.md) §4; its networks decoded through their lowering, [integer-operators](extensions/integer-operators.md) §13.2 (`whm1/1`) | -| `wai.neural.int_mlicpp` | integer fixed-point **MLIC++ entropy topology** — `K` channel slices of the latent, each decoded as a checkerboard anchor pass then a non-anchor pass, with an entropy network over concat[latent decoded so far, hyper] re-run before every pass to give σ‖μ; hyper-decoder, entropy network and synthesis all integer | the channel-autoregressive × checkerboard entropy model at **decode-equivalence**: the encoder and every decoder compute the same σ, so the same distribution for every element, at any thread count; bit-identical on every machine. The committed conformance vector has synthetic weights, so it demonstrates decode-equivalence and states no quality figure | `int_mlicpp.rs` (one-file model bundle sink-supplied; `tools/wai_int_mlicpp_export.py`) | +| `wai.neural.int_mlicpp` | integer fixed-point **MLIC++ entropy topology** — `K` channel slices of the latent, each decoded as a checkerboard anchor pass then a non-anchor pass, with an entropy network over concat[latent decoded so far, hyper] re-run before every pass to give σ‖μ; hyper-decoder, entropy network and synthesis all integer | the channel-autoregressive × checkerboard entropy model at **decode-equivalence**: the encoder and every decoder compute the same σ, so the same distribution for every element, at any thread count; bit-identical on every machine. The committed conformance vector has synthetic weights, so it demonstrates decode-equivalence and states no quality figure | `int_mlicpp.rs` (one-file model bundle sink-supplied; `tools/wai_int_mlicpp_export.py`). Payload and bundle: [integer-payloads](extensions/integer-payloads.md) §14; its networks decoded through their lowering, [integer-operators](extensions/integer-operators.md) §13.4 (`wmb1/1`) | | `wai.image.jpegai` | JPEG AI (ISO/IEC 6048-1:2025) codestream → RGB — integer entropy front end (mANS, with an integer scale-index derivation) + 16-bit-PTQ integer hyper-decoder, context-model and synthesis networks + dequantisation, latent refinement, post-filters (the eICCI CNN among them) and colour conversion in IEEE-754 float with a fixed evaluation order. Scope: one configuration — 256×256 4:4:4, `model_id` 0 (tools_0), `beta_displacement_log` 0, the high-operation-point synthesis, the eICCI model pair the weight bundle carries, and the other header conditions in [determinism-tiers](extensions/determinism-tiers.md) §5; both decode paths refuse any other codestream. Within it, a sink without the integer weight bundle, or in a non-default floating-point environment, takes a float ONNX decode instead. Only checkpoint constants are sink-supplied | **entropy-consistency** (registry class `NeuralEntropyExact`): the entropy decode, including the scale-index derivation, is integer on both decode paths, so the decoded symbols reproduce on every machine. The exact decoder also pins one reconstruction to RGB, reproducible across machines under the build and floating-point conditions in [determinism-tiers](extensions/determinism-tiers.md) §5–§6 (it checks the floating-point environment before decoding) — a property of that decoder, not of the capability, so `wai.image.jpegai` does not back `replicate` (§7). On one reference codestream the exact decoder stays within max\|Δ\| ≤ 2 of the reference decoder (recorded max\|Δ\| = 1 on ~4.4% of samples); evidence in [determinism-tiers](extensions/determinism-tiers.md) §6. **Downgraded in v0.4.0:** this row previously claimed decode-equivalence, and the registry classed the capability `StandardDefined` | `jpegai_decode.rs` (+ `jpegai_{bitstream,mans,pipeline,synthesis,icci}.rs`) | | `wai.generate.spectral` | deterministic integer GENERATIVE synthesis (`intent=create`) — prompt+seed → integer-LUT spectral composition, no float/ONNX | byte-identical on every machine, pinned to (model, prompt, seed, steps); the create-side of "conformance is a hash" (generation-equivalence). Payload: the instruction object of form v0, which names the model by its SHA-256 and lists every size a sink makes, each output checked by its SHA-256 (*Payload of `wai.generate.spectral`*, below) | `generate.rs` (small pinned model bundle) | | `wai.neural.elic` | checkerboard context model (hyperprior + 2-pass spatial context) | the checkerboard-context entropy class (arXiv 2203.10886) | `neural/context.rs` | @@ -1513,7 +1513,8 @@ registry as `codecs::capability_replicate_class(cap) -> ReplicateClass`: are lowered by, the profile every conforming set's certificate meets, and the modes of its stage operators. A set whose certificate does not meet that profile is refused. In this revision: `wai.neural.int_synth`, `wai.splat.int_codec`, - `wai.audio.int_codec`, `wai.neural.int_hyper` and `wai.video.int_hyper`. + `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and + `wai.neural.int_mlicpp`. - **`NeuralEntropyExact`** — the entropy decode is integer by construction, so the decoded symbols are byte-identical on every machine; later stages run float. Registered at `entropy-consistency`. An encoder MUST NOT emit `intent = diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index 998c8a3ee..9b1cde1b6 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -1363,6 +1363,10 @@ pub fn capability_ops(cap: &str) -> Option { // `ceil` rule (integer-payloads §4 step 3); the keyframe of // `wai.video.int_hyper` is that codec, and its residual stream is §2's too. CAP_NEURAL_INT_HYPER | CAP_VIDEO_INT_HYPER => ops("whm1/1", PAYLOAD_STREAM, Some("ceil")), + // The MLIC++-topology codec's two streams decode with E1's `t2` table rule and its + // `final_only` end rule, and each latent's σ is bucketed with E3's `ceil` rule + // (integer-payloads §14). + CAP_NEURAL_INT_MLICPP => ops("wmb1/1", Some(("t2", "lenient", "final_only")), Some("ceil")), _ => None, } } diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 6ca8f3092..030ace71c 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -1421,14 +1421,14 @@ mod tests { /// `NeuralIntegerExact` capability carries one, every one is at `acc64` with the /// full subset and names a lowering of §13, and the export writes exactly these. /// In this revision `wai.neural.int_synth`, `wai.splat.int_codec`, - /// `wai.audio.int_codec`, `wai.neural.int_hyper` and `wai.video.int_hyper` decode - /// through the operator set. + /// `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and + /// `wai.neural.int_mlicpp` decode through the operator set. #[test] fn ops_members_name_the_operator_set_a_capability_decodes_through() { let regs = registrations(); let mut with: Vec<&str> = regs.iter().filter(|r| r.ops.is_some()).map(|r| r.capability).collect(); with.sort_unstable(); - assert_eq!(with, [CAP_AUDIO_INT_CODEC, CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_SPLAT_INT_CODEC, CAP_VIDEO_INT_HYPER]); + assert_eq!(with, [CAP_AUDIO_INT_CODEC, CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_MLICPP, CAP_NEURAL_INT_SYNTH, CAP_SPLAT_INT_CODEC, CAP_VIDEO_INT_HYPER]); for r in regs.iter().filter(|r| r.ops.is_some()) { let o = r.ops.unwrap(); assert_eq!(r.replicate, Some(ReplicateClass::NeuralIntegerExact), "{}", r.capability); @@ -1451,6 +1451,10 @@ mod tests { "set": "wai-ops/1", "profile": "acc64", "subset": "full", "lowering": "whm1/1", "stage": { "rans64": { "table": "t4097", "escape": "lenient", "end": "canonical" }, "bucket": "ceil" } }), + CAP_NEURAL_INT_MLICPP => serde_json::json!({ + "set": "wai-ops/1", "profile": "acc64", "subset": "full", "lowering": "wmb1/1", + "stage": { "rans64": { "table": "t2", "escape": "lenient", "end": "final_only" }, "bucket": "ceil" } + }), _ => serde_json::Value::Null, }; assert_eq!(c["ops"], want, "{}", c["capability"]); diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs index abd199206..2804ef684 100644 --- a/wai/wai-rs/tests/int_mlicpp_ops.rs +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -208,3 +208,254 @@ fn the_committed_vector_decodes_through_the_lowering_to_its_golden() { assert_eq!((b.as_str(), s.as_str()), (exp["bucket_fnv1a"].as_str().unwrap(), exp["symbol_fnv1a"].as_str().unwrap())); assert_eq!(t_ops.buckets.len() as u64, exp["y_symbols"].as_u64().unwrap()); } + +/// The code integer-payloads §8 gives a refusal of the decode, as `int_payload::decode_int` +/// reports it: the payload's, or the bundle's against it. +fn code_of(e: &mlic::IntMlicError) -> &'static str { + match e { + mlic::IntMlicError::Payload(_) => "payload_malformed", + mlic::IntMlicError::Model(_) => "model_mismatch", + } +} + +/// A `WIM1` payload's fields, owned, so a fixture can change any of them: `dims` are +/// `Cz, Zh, Zw, Cy, Yh, Yw`. +#[derive(Clone)] +struct Wim1 { + h: u32, + w: u32, + dims: [u16; 6], + k: u16, + reserved: u16, + z: Vec, + y: Vec, +} + +impl Wim1 { + fn committed() -> Wim1 { + let p = payload_of("stream.wai"); + let u16at = |o: usize| u16::from_le_bytes([p[o], p[o + 1]]); + let u32at = |o: usize| u32::from_le_bytes(p[o..o + 4].try_into().unwrap()); + let (lz, ly) = (u32at(28) as usize, u32at(32) as usize); + Wim1 { + h: u32at(4), w: u32at(8), dims: [12, 14, 16, 18, 20, 22].map(u16at), k: u16at(24), reserved: u16at(26), + z: p[36..36 + lz].to_vec(), y: p[36 + lz..36 + lz + ly].to_vec(), + } + } + fn bytes(&self) -> Vec { + let mut p = b"WIM1".to_vec(); + p.extend(self.h.to_le_bytes()); + p.extend(self.w.to_le_bytes()); + for d in &self.dims { + p.extend(d.to_le_bytes()); + } + p.extend(self.k.to_le_bytes()); + p.extend(self.reserved.to_le_bytes()); + p.extend((self.z.len() as u32).to_le_bytes()); + p.extend((self.y.len() as u32).to_le_bytes()); + p.extend(&self.z); + p.extend(&self.y); + p + } +} + +/// The committed payload, changed by `f`. +fn wim1(f: impl FnOnce(&mut Wim1)) -> Vec { + let mut o = Wim1::committed(); + f(&mut o); + o.bytes() +} + +/// The committed bundle, parsed, changed by `f`. +fn bundle(f: impl FnOnce(&mut IntMlicModel)) -> IntMlicModel { + let mut m = model(); + f(&mut m); + m +} + +/// Both decodes of a payload against a bundle, with their traces: through the lowering, +/// then directly. A refusal is its code. +type Decoded = (mlic::Trace, Result, &'static str>); +fn both(m: &IntMlicModel, p: &[u8]) -> (Decoded, Decoded) { + let f = |(t, r): (mlic::Trace, Result)| (t, r.map(|i| i.rgb).map_err(|e| code_of(&e))); + (f(mlic::ops::decode_traced(m, p)), f(mlic::legacy::decode_traced(m, p))) +} + +/// A layer's biases, in place. +fn bias(l: &mut wai::int_hyper_synth::IntLayer) -> &mut Vec { + use wai::int_hyper_synth::IntLayer; + match l { + IntLayer::Conv { bias, .. } | IntLayer::ConvT { bias, .. } => bias, + _ => panic!("not a convolution"), + } +} + +/// Gate 3 of the migration: refusal parity. A fixture for each refusal the direct decode +/// makes (integer-payloads §14, in its order: the header, the bundle against the header +/// and the networks' geometry, then each stream and each network as it runs), each made +/// from the committed bundle and payload by one change. Each is refused by both decodes +/// with the same code (integer-payloads §8: `payload_malformed` for the payload and the +/// decode, `model_mismatch` for a bundle that does not fit the payload), as far as the +/// same `y` symbol. The committed bundle and payload decode alike. Then the sink's codes +/// for two committed cases of `int_payload`: a payload that is not one, and a bundle that +/// is not one. +#[test] +fn every_refusal_of_the_direct_decode_is_refused_alike_through_the_lowering() { + use wai::int_hyper_synth::IntLayer; + let (ops, direct) = both(&model(), &Wim1::committed().bytes()); + assert!(ops.1.is_ok() && ops == direct, "the fixtures' base decodes alike"); + let same = model; + let as_is = || Wim1::committed().bytes(); + // A stream moved: z cut to `lz` bytes, y to `ly` (each from its start). + let cut = |lz: usize, ly: usize| wim1(|o| { o.z.truncate(lz); o.y.truncate(ly); }); + let (lz, ly) = { let o = Wim1::committed(); (o.z.len(), o.y.len()) }; + // A synthesis that spreads the latent over a plane past the cap and brings it back: + // the right output, through a buffer over 2^26 elements. + let wide = || bundle(|m| { + let s = 1usize << 20; + let unit = |out: usize| (vec![1i64; 8 * out], vec![1i64; out], vec![0i64; out]); + let (weight, mult, b) = unit(8); + m.g_s.layers.insert(0, IntLayer::ConvT { out_ch: 8, k: 1, stride: s, pad: 0, opad: 0, weight, mult, bias: b }); + let (weight, mult, b) = unit(8); + m.g_s.layers.insert(1, IntLayer::Conv { out_ch: 8, k: 1, stride: s, pad: 0, weight, mult, bias: b }); + }); + let fixtures: Vec<(&str, &str, IntMlicModel, Vec)> = vec![ + ("a payload shorter than its header", "payload_malformed", same(), as_is()[..35].to_vec()), + ("not WIM1", "payload_malformed", same(), { let mut p = as_is(); p[3] = b'2'; p }), + ("a reserved field that is not 0", "payload_malformed", same(), wim1(|o| o.reserved = 1)), + ("a dimension of 0", "payload_malformed", same(), wim1(|o| o.dims[3] = 0)), + ("Cy not a multiple of K", "payload_malformed", same(), wim1(|o| o.k = 3)), + ("an image past 2^26 elements", "payload_malformed", same(), wim1(|o| (o.h, o.w) = (1 << 14, 1 << 14))), + ("lengths that are not the payload's", "payload_malformed", same(), [as_is(), vec![0]].concat()), + ("more hyper-latent channels than z tables", "model_mismatch", bundle(|m| { + let t = &mut m.tables; + (t.z_cdfs.pop(), t.z_cdf_lengths.pop(), t.z_offsets.pop(), t.z_median_q.pop()); + }), as_is()), + ("an h_s that does not give the declared latent", "model_mismatch", same(), wim1(|o| o.dims[4] = 9)), + ("an entropy network that does not give 2·Cy channels", "model_mismatch", bundle(|m| { m.entropy.layers.pop(); }), as_is()), + ("a g_s that does not give the declared image", "model_mismatch", same(), wim1(|o| o.h = 65)), + ("a synthesis through a buffer past 2^26 elements", "model_mismatch", wide(), as_is()), + ("a z stream too short to be one", "payload_malformed", same(), cut(4, ly)), + ("a z value that leaves i64", "payload_malformed", bundle(|m| m.tables.z_median_q = vec![i64::MAX; 4]), as_is()), + ("a z stream that runs out", "payload_malformed", same(), cut(lz - 4, ly)), + ("hyper-latents past h_s's range", "payload_malformed", bundle(|m| m.tables.z_median_q = vec![1 << 46; 4]), as_is()), + ("a y stream too short to be one", "payload_malformed", same(), cut(lz, 4)), + ("hyper past the entropy network's range", "payload_malformed", bundle(|m| bias(&mut m.h_s.layers[2]).iter_mut().for_each(|b| *b = 1 << 50)), as_is()), + ("means near i64's end", "payload_malformed", bundle(|m| bias(&mut m.entropy.layers[2])[8..].iter_mut().for_each(|b| *b = i64::MAX - (1 << 20))), as_is()), + ("a y stream that runs out", "payload_malformed", same(), cut(lz, ly - 4)), + ("latents past g_s's range", "payload_malformed", bundle(|m| bias(&mut m.g_s.layers[0])[0] = i64::MAX - (1 << 20)), as_is()), + ]; + for (what, want, m, p) in &fixtures { + let (ops, direct) = both(m, p); + assert_eq!((ops.1.as_ref().err(), direct.1.as_ref().err()), (Some(want), Some(want)), "{what}: through the lowering, then directly"); + assert!(ops.0 == direct.0, "{what}: the two decodes read different symbols before the refusal"); + } + // The sink's codes (`int_payload/tests.rs`' committed cases). + let cap = "wai.neural.int_mlicpp"; + let bundle_bytes = read(&format!("{DIR}/model.wmb")); + let sink = |p: &[u8], set: &[u8]| { + let params = wai::int_payload::resolve(cap, None, &[wai::pin::HeldBytes::File(set)], None).expect("an unpinned set resolves"); + wai::int_payload::decode_int(cap, p, ¶ms).map(|_| ()).map_err(|e| e.code()) + }; + assert_eq!(sink(b"WIM1", &bundle_bytes), Err("payload_malformed")); + assert_eq!(sink(&as_is(), b"WHM1"), Err("model_mismatch")); + assert_eq!(sink(&as_is(), &bundle_bytes), Ok(())); +} + +/// Gate 3, the end rule `final_only` (integer-payloads §14; integer-operators E1): a +/// stream that runs out is refused once, after its last symbol. The `y` stream cut short by +/// 4 to 64 bytes runs out part of the way through, and both decodes still decode every +/// symbol after that point, from the state as it stands, feed each to the passes after it +/// (the entropy network's input, in which every element of `y` not yet decoded is 0), and +/// refuse at the end, with the same buckets and symbols. Where a symbol decoded after the +/// run-out gives a refusal of its own before the end (a value that leaves `i64`, or one +/// outside the entropy network's range), both give that one, at the same symbol. The +/// `z` stream cut short is refused alike too. +#[test] +fn a_stream_that_runs_out_is_refused_at_its_end_alike() { + let m = model(); + let full = { let (t, r) = mlic::ops::decode_traced(&m, &Wim1::committed().bytes()); r.unwrap(); t.buckets.len() }; + let msg = |p: &[u8]| match mlic::ops::decode(&m, p) { + Err(mlic::IntMlicError::Payload(e)) => e, + other => panic!("{:?}", other.map(|_| ())), + }; + let (mut at_end, mut before) = (0, 0); + for cut in (4..=64).step_by(4) { + let p = wim1(|o| { let n = o.y.len() - cut; o.y.truncate(n); }); + let (ops, direct) = both(&m, &p); + assert!(ops == direct, "y cut by {cut}: the two decodes differ"); + assert_eq!(ops.1, Err("payload_malformed"), "y cut by {cut}"); + if ops.0.buckets.len() == full { + assert_eq!(msg(&p), "y stream ended before its last symbol", "y cut by {cut}: every symbol decoded, then refused at the end"); + at_end += 1; + } else { + before += 1; + } + } + eprintln!("y cut short: {at_end} decoded to the last symbol and refused at the end, {before} refused before it"); + assert!(at_end > 0, "a stream that runs out is decoded to its last symbol before it is refused"); + for cut in [4, 8] { + let p = wim1(|o| { let n = o.z.len() - cut; o.z.truncate(n); }); + let (ops, direct) = both(&m, &p); + assert!(ops == direct && ops.1 == Err("payload_malformed"), "z cut by {cut}"); + } +} + +/// Gate 3, at the edge of every `check` range: for each layer of the committed `h_s`, +/// `entropy` and `g_s` that reads one (§13.2: a layer of tag 0, 1, 2 or 4; a ReLU's input +/// has none), a network of that layer alone (with the bundle's header; a leaky ReLU followed +/// by a zero 1×1 transposed convolution, since a network with no layer of tag 0, 1 or 2 does +/// not lower) is lowered, and its input's `check` range is `[−X_ℓ, X_ℓ]`, the range the full +/// graph gives tensor `ℓ`. An input whose largest magnitude is `X_ℓ` runs, through the +/// lowering and directly, to the same output; one at `X_ℓ + 1` is refused by both: by the +/// graph (`field_range`, at input 0) and by the direct decode's check of that layer +/// (`run_net_checked`, a value out of range at layer 0). So the ranges refuse exactly what +/// the direct decode refuses, at the same layer. +#[test] +fn every_check_range_refuses_exactly_what_the_direct_decode_refuses() { + use wai::int_hyper_synth::{run_net_checked, IntLayer}; + use wai_ops::{Code, Site, Tensor}; + let secs = sections(&read(&format!("{DIR}/model.wmb"))); + let mut seen = 0; + for (k, (net, rgb8)) in [("h_s", false), ("entropy", false), ("g_s", true)].into_iter().enumerate() { + let set = &secs[k]; + let full = wai_ops::lower::whm1(set, rgb8).unwrap(); + let n = wai_ops::lower::parse_whm1(set).unwrap().layers.len(); + for l in 0..n { + let mut one = IntModel::from_bytes(set).unwrap(); + let layer = one.layers.swap_remove(l); + if matches!(layer, IntLayer::Relu) { + continue; + } + let leaky = matches!(layer, IntLayer::Leaky { .. }); + one.layers = vec![layer]; + let c = full.tensors[l].channels; + if leaky { + one.layers.push(IntLayer::ConvT { out_ch: 1, k: 1, stride: 1, pad: 0, opad: 0, weight: vec![0; c as usize], mult: vec![1], bias: vec![0] }); + } + let g = wai_ops::lower::whm1_net(&wai::int_ops::whm1_net(&one).unwrap(), false).unwrap(); + let (lo, hi) = (g.tensors[0].lo, g.tensors[0].hi); + assert_eq!((g.tensors[0].channels, lo, hi), (c, full.tensors[l].lo, full.tensors[l].hi), "{net} layer {l}: the range of tensor {l}"); + assert!(hi > 0 && lo == -hi, "{net} layer {l}: a symmetric check range [{lo}, {hi}]"); + let loaded = wai::int_ops::load(&g).unwrap(); + let x = |v: i64| { + let mut d = vec![0i64; c as usize * 4]; + d[c as usize * 2 + 1] = v; + d + }; + for v in [hi, -hi] { + let ops = wai::int_ops::run1(&loaded, Tensor::new(c, 2, 2, x(v)), &[]).unwrap_or_else(|r| panic!("{net} layer {l}: {v} runs: {r:?}")); + let (direct, ..) = run_net_checked(&one, &x(v), c as usize, 2, 2).unwrap_or_else(|r| panic!("{net} layer {l}: {v} runs directly: {r}")); + assert!(ops.data == direct, "{net} layer {l}: at {v} the two differ"); + } + for v in [hi + 1, -hi - 1] { + let r = wai::int_ops::run1(&loaded, Tensor::new(c, 2, 2, x(v)), &[]).expect_err("one past the range"); + assert_eq!((r.code, r.at), (Code::FieldRange, Site::Input(0)), "{net} layer {l}: {v}"); + let d = run_net_checked(&one, &x(v), c as usize, 2, 2).expect_err("one past the range, directly"); + assert!(d.out_of_range && d.layer == 0, "{net} layer {l}: {v}: {d}"); + } + seen += 1; + } + } + assert_eq!(seen, 10, "every layer of the committed h_s, entropy and g_s that reads a check range"); +} -- 2.54.0 (Apple Git-157) From ba9a0b2b1315f0ad97e25a05041dd5c8a300b450 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 21:52:21 -0400 Subject: [PATCH 14/22] int_mlicpp gate 3: the registry export Co-Authored-By: Claude Opus 5.5 --- wai/registry/capabilities.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index c6fe3a934..f49193baa 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -752,7 +752,21 @@ "sets": [], "spec_section": "§5 Neural capabilities", "edition": null, - "registered": "2026-09-30" + "registered": "2026-09-30", + "ops": { + "set": "wai-ops/1", + "profile": "acc64", + "subset": "full", + "lowering": "wmb1/1", + "stage": { + "rans64": { + "table": "t2", + "escape": "lenient", + "end": "final_only" + }, + "bucket": "ceil" + } + } }, { "capability": "wai.neural.int_synth", -- 2.54.0 (Apple Git-157) From 482865908c9d4d7de07afb2229f45d3a516a5255 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 21:55:11 -0400 Subject: [PATCH 15/22] int_mlicpp gate 3: the entropy fixture gives 4 channels; the end-rule test cuts the y stream at every length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed entropy network's first convolution already gives 2·Cy channels, so the fixture that dropped its last layer still fit; it now ends with a 1x1 convolution to 4. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/int_mlicpp_ops.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs index 2804ef684..6df99e712 100644 --- a/wai/wai-rs/tests/int_mlicpp_ops.rs +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -332,7 +332,10 @@ fn every_refusal_of_the_direct_decode_is_refused_alike_through_the_lowering() { (t.z_cdfs.pop(), t.z_cdf_lengths.pop(), t.z_offsets.pop(), t.z_median_q.pop()); }), as_is()), ("an h_s that does not give the declared latent", "model_mismatch", same(), wim1(|o| o.dims[4] = 9)), - ("an entropy network that does not give 2·Cy channels", "model_mismatch", bundle(|m| { m.entropy.layers.pop(); }), as_is()), + ("an entropy network that does not give 2·Cy channels", "model_mismatch", bundle(|m| { + let to4 = IntLayer::Conv { out_ch: 4, k: 1, stride: 1, pad: 0, weight: vec![1; 16 * 4], mult: vec![1; 4], bias: vec![0; 4] }; + m.entropy.layers.push(to4); + }), as_is()), ("a g_s that does not give the declared image", "model_mismatch", same(), wim1(|o| o.h = 65)), ("a synthesis through a buffer past 2^26 elements", "model_mismatch", wide(), as_is()), ("a z stream too short to be one", "payload_malformed", same(), cut(4, ly)), @@ -364,7 +367,8 @@ fn every_refusal_of_the_direct_decode_is_refused_alike_through_the_lowering() { /// Gate 3, the end rule `final_only` (integer-payloads §14; integer-operators E1): a /// stream that runs out is refused once, after its last symbol. The `y` stream cut short by -/// 4 to 64 bytes runs out part of the way through, and both decodes still decode every +/// every multiple of 4 bytes that leaves it a stream runs out part of the way through, and +/// both decodes still decode every /// symbol after that point, from the state as it stands, feed each to the passes after it /// (the entropy network's input, in which every element of `y` not yet decoded is 0), and /// refuse at the end, with the same buckets and symbols. Where a symbol decoded after the @@ -380,7 +384,7 @@ fn a_stream_that_runs_out_is_refused_at_its_end_alike() { other => panic!("{:?}", other.map(|_| ())), }; let (mut at_end, mut before) = (0, 0); - for cut in (4..=64).step_by(4) { + for cut in (4..=Wim1::committed().y.len() - 8).step_by(4) { let p = wim1(|o| { let n = o.y.len() - cut; o.y.truncate(n); }); let (ops, direct) = both(&m, &p); assert!(ops == direct, "y cut by {cut}: the two decodes differ"); -- 2.54.0 (Apple Git-157) From 01753591675cc4fad08ed5d367ddc8a8a3efa768 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:02:26 -0400 Subject: [PATCH 16/22] int_mlicpp gate 4 (WIP: committed before running): the differential's int_mlicpp half tests/int_ops_differential.rs: random WMB1 bundles (h_s, an entropy network that keeps the size, g_s, the tables) and WIM1 payloads whose y stream is coded in the decode's order against the buckets the direct networks give; each case through int_mlicpp::ops and int_mlicpp::legacy: outside the planned breaks the same y symbols and the same image, or the same code at the same check; under a planned break the decode through the operator set refuses (model_mismatch). The classifier reads the bundle and the payload's header: the other WHM1 decoders' breaks (load_break), and the rules the direct decode makes only when a network runs (a layer that does not fit the channels before it, a shift or gf of 128 or more that a layer uses, a leaky shift of 64 or more, a network whose input is not the payload's channel count), and a convolution's padded side past 2^26, which the operator set's run-time plan caps. Each break is shown on a hand-made bundle. The bounds-pass oracle test also covers the three networks. The nightly workflow names the decoder. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-ops-differential.yml | 4 +- wai/wai-rs/tests/int_ops_differential.rs | 506 ++++++++++++++++++++- 2 files changed, 506 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wai-ops-differential.yml b/.github/workflows/wai-ops-differential.yml index 31ad89e93..eac1e88b3 100644 --- a/.github/workflows/wai-ops-differential.yml +++ b/.github/workflows/wai-ops-differential.yml @@ -7,8 +7,8 @@ name: wai-ops differential (nightly) # of integer-operators §15.3 the two decode to the same bytes or both refuse, and the # direct decode never panics; under a planned break the decode through the operator set # refuses (wai/wai-rs/tests/int_ops_differential.rs). Today: wai.neural.int_synth, -# wai.splat.int_codec, wai.audio.int_codec and wai.neural.int_hyper (whose decode is -# also the keyframe of wai.video.int_hyper). +# wai.splat.int_codec, wai.audio.int_codec, wai.neural.int_hyper (whose decode is +# also the keyframe of wai.video.int_hyper) and wai.neural.int_mlicpp. # # Each night starts from its own seed, the date, which the log prints; a failing case # names its seed, and `WAI_DIFF_SEED=` re-runs it. The same tests run for a fixed diff --git a/wai/wai-rs/tests/int_ops_differential.rs b/wai/wai-rs/tests/int_ops_differential.rs index 16ee75dc3..fe8325c5f 100644 --- a/wai/wai-rs/tests/int_ops_differential.rs +++ b/wai/wai-rs/tests/int_ops_differential.rs @@ -7,8 +7,9 @@ //! through the operator set, whatever the direct decode does there. //! //! The decoders: `wai.neural.int_synth` (`int_synth/1`), `wai.splat.int_codec` -//! (`whm1/1`), `wai.audio.int_codec` (`wam1/1`) and `wai.neural.int_hyper` (`whm1/1`, its -//! two networks; the keyframe of `wai.video.int_hyper` is the same decode). +//! (`whm1/1`), `wai.audio.int_codec` (`wam1/1`), `wai.neural.int_hyper` (`whm1/1`, its +//! two networks; the keyframe of `wai.video.int_hyper` is the same decode) and +//! `wai.neural.int_mlicpp` (`wmb1/1`, its three networks). //! //! The same generators hold load step 8 to its oracle: the bounds pass, whose //! convolutions and inverse GDNs are in closed form, against the pass as it stood before @@ -1368,6 +1369,499 @@ fn int_hyper_planned_breaks_are_new_refusals() { expect("a check range its producer cannot meet", &whm1_bytes(12, 16, 0, &[first, tight]), &h0, &cl, Some(RefusalKind::FieldRange)); } +// ------------------------------------------------------------------ int_mlicpp (M5) + +/// A `WMB1` bundle's bytes: the three `WHM1` networks, then the tables (with `afrac`). +fn wmb1_bytes(h_s: &[u8], entropy: &[u8], g_s: &[u8], tables: &Value) -> Vec { + let t = serde_json::to_vec(tables).unwrap(); + let mut v = b"WMB1".to_vec(); + for s in [h_s, entropy, g_s, t.as_slice()] { + v.extend((s.len() as u32).to_le_bytes()); + v.extend(s); + } + v +} + +/// A `WMB1` bundle's four sections. +fn wmb1_sections(b: &[u8]) -> Option<[Vec; 4]> { + let mut o = 4; + let mut s: [Vec; 4] = Default::default(); + for x in &mut s { + let n = u32::from_le_bytes(b.get(o..o + 4)?.try_into().ok()?) as usize; + *x = b.get(o + 4..o + 4 + n)?.to_vec(); + o += 4 + n; + } + Some(s) +} + +/// A `WIM1` payload's bytes: `H`, `W`; `Cz, Zh, Zw, Cy, Yh, Yw, K`; the two streams. +fn wim1_bytes(h: u32, w: u32, dims: [u16; 7], z: &[u8], y: &[u8]) -> Vec { + let mut p = b"WIM1".to_vec(); + p.extend(h.to_le_bytes()); + p.extend(w.to_le_bytes()); + dims.iter().for_each(|d| p.extend(d.to_le_bytes())); + p.extend(0u16.to_le_bytes()); + p.extend((z.len() as u32).to_le_bytes()); + p.extend((y.len() as u32).to_le_bytes()); + p.extend(z); + p.extend(y); + p +} + +/// The planned break of integer-operators §15.3 that a `wai.neural.int_mlicpp` case falls +/// under, if any, read from the bundle and the payload's header alone. A bundle that does +/// not parse, or a header or bundle that the checks both decodes make first refuse +/// (`int_mlicpp::ops::check`: the header, the `z` tables, each network's geometry on the +/// header's shapes), is not a break: both refuse it alike. Otherwise the decode through the +/// operator set loads the three networks (`h_s`, `entropy`, `g_s` with the RGB8 step, by +/// `whm1/1` under `acc64`) and plans each on the payload's shapes before either stream, +/// and refuses (`model_mismatch`) a bundle that: +/// - falls under one of the breaks of the other `WHM1` decoders ([`load_break`]); +/// - has a layer that does not fit the channels the layer before it gives, a header shift +/// or `gf` of 128 or more that a layer uses, or a leaky ReLU's shift of 64 or more, which +/// the direct decode refuses (`model_mismatch` too) only when that network runs, after +/// the streams and networks before it, so an earlier refusal of the payload comes first +/// there; +/// - has a network whose input is not the payload's channel count (`Cz`, `3·Cy`, `Cy`), +/// which the direct decode likewise refuses when that network runs; or a convolution +/// whose padded side exceeds `2^26`, which the operator set's run-time plan caps +/// (integer-operators §5.2) and the direct decode's geometry does not. +fn int_mlicpp_planned_break(bundle: &[u8], payload: &[u8]) -> Option<&'static str> { + use wai_ops::lower::NetLayer as N; + let m = wai::int_mlicpp::IntMlicModel::from_bundle(bundle).ok()?; + let hd = wai::int_mlicpp::ops::check(&m, payload).ok()?; + let secs = wmb1_sections(bundle)?; + for (k, rgb8) in [(0, false), (1, false), (2, true)] { + let net = wai_ops::lower::parse_whm1(&secs[k]).ok()?; + if let Some(b) = load_break(&net, wai_ops::lower::whm1(&secs[k], rgb8)) { + return Some(b); + } + let conv = net.layers.iter().any(|l| matches!(l, N::ConvT { .. } | N::Conv { .. })); + let igdn = net.layers.iter().any(|l| matches!(l, N::Igdn { .. })); + if (net.shift > 127 && conv) || (net.gf > 127 && igdn) { + return Some("a shift or gf of 128 or more that a layer uses"); + } + if net.layers.iter().any(|l| matches!(l, N::Leaky { slope_frac, .. } if *slope_frac > 63)) { + return Some("a leaky ReLU's shift of 64 or more"); + } + let mut c: Option = None; + for l in &net.layers { + let (inc, out) = match l { + N::ConvT { inc, out, .. } | N::Conv { inc, out, .. } => (*inc, *out), + N::Igdn { c, .. } => (*c, *c), + _ => continue, + }; + if c.is_some_and(|c| c != inc) { + return Some("a layer that does not fit the channels before it"); + } + c = Some(out); + } + } + let n = wai::int_mlicpp::ops::load(&m).ok()?; + let (cy, yh, yw) = (hd.cy as u64, hd.yh as u64, hd.yw as u64); + for (g, input) in [(n.h_s.graph(), (hd.cz as u64, hd.zh as u64, hd.zw as u64)), (n.entropy.graph(), (3 * cy, yh, yw)), (n.g_s.graph(), (cy, yh, yw))] { + match wai_ops::plan::run_plan(g, &[input], &[]) { + Err(r) if r.code == wai_ops::Code::ModelMismatch && r.at == wai_ops::Site::Input(0) => { + return Some("a network whose input is not the payload's channel count"); + } + Err(r) if r.code == wai_ops::Code::TooLarge => return Some("a convolution's padded side past 2^26"), + _ => {} + } + } + None +} + +/// A refusal of either `int_mlicpp` decode, as both name it: the code (integer-payloads §8), +/// then which check: for a network as it runs, the network (the direct decode names the +/// layer's arithmetic, the operator set the `check` range); for any other, the decode's +/// own message, which the two share. +fn mlic_label(e: &wai::int_mlicpp::IntMlicError) -> String { + use wai::int_mlicpp::IntMlicError as E; + let (code, m) = match e { + E::Payload(m) => ("payload_malformed", m.as_str()), + E::Model(m) => ("model_mismatch", m.as_str()), + }; + let m = match m.find(" refused") { + Some(i) if ["h_s", "entropy", "g_s"].contains(&&m[..i]) => &m[..i], + _ => m, + }; + format!("{code}: {m}") +} + +/// One `int_mlicpp` decode's outcome: the `y` symbols it read (buckets and symbols), and +/// the image or its refusal's label. +type MlicOutcome = (Vec, Vec, Result, String>); + +fn mlic_outcome((t, r): (wai::int_mlicpp::Trace, Result)) -> MlicOutcome { + (t.buckets, t.symbols, r.map(|i| i.rgb).map_err(|e| mlic_label(&e))) +} + +/// A last layer for an entropy network: a stride-1 convolution from `c` channels to `out` +/// that keeps the size (kernel 1 or 3, padding `k / 2`; one in sixteen another padding), +/// with `splat_layers`' parameter draws. +fn same_size_layer(r: &mut Rng, c: u32, out: u32) -> Whm { + let k = [1u32, 3][r.below(2) as usize]; + let p = if r.below(16) == 0 { r.below(3) as u32 } else { k / 2 }; + let wmax = if r.below(8) == 0 { 32767 } else { [16, 1024, 4096][r.below(3) as usize] }; + let w: Vec = (0..c * out * k * k).map(|_| if r.below(4) == 0 { 0 } else { r.signed(wmax) as i16 }).collect(); + let m: Vec = (0..out).map(|_| if r.below(32) == 0 { r.signed(1 << 40) } else { r.signed(1 << 12) }).collect(); + let b: Vec = (0..out) + .map(|_| match r.below(64) { + 0 => [i64::MAX, i64::MIN, i64::MIN + 1, i64::MAX - 1][r.below(4) as usize], + 1 => r.signed(1 << 40), + _ => r.signed(1 << 20), + }) + .collect(); + Whm::Conv { inc: c, out, k, s: 1, p, w, m, b } +} + +/// A random `wai.neural.int_mlicpp` bundle and `WIM1` payload. The bundle: `afrac` 12 +/// mostly (one in sixteen anywhere in 1..=30, one in sixteen at 0 or 31, and one network's +/// not the others' one in 32); each network's header `gf` or `shift` of 128 or more one in +/// 32 each; `h_s` from `Cz` (1 to 3) channels, up to one of `splat_layers`' layers then a +/// last layer to `2·Cy` (one in sixteen another count); `entropy` from `3·Cy`, up to one +/// layer that keeps the size (a leaky ReLU, a ReLU or an inverse GDN, or one in four +/// `splat_layers`' draw), then [`same_size_layer`] to `2·Cy` (one in sixteen another +/// count); `g_s` from `Cy` (1, 2 or 4), up to two layers then a last layer to three +/// channels (one in sixteen not); the tables as [`int_hyper_case`]'s, with `afrac`. The +/// payload: `K` a divisor of `Cy` (one in 32 not); a hyper-latent of side 2 or 3 (one in +/// sixteen not square), its symbols as [`int_hyper_case`]'s, one case in four with a whole +/// channel at `h_s`'s input `check` range or one past it; the latent's size `h_s`'s output +/// and the image's `g_s`'s (each one in eight off by one); the `y` stream coded in the +/// decode's order (each slice's anchors, then the rest), each symbol against the table the +/// direct networks' `σ` buckets it to, mostly inside its table, some escaped and some far, +/// until a network refuses (the rest of the stream is then random); each stream damaged as +/// [`damage`] says. +fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { + use wai::int_hyper_synth::run_net_checked; + let afrac = match r.below(16) { + 0 => 1 + r.below(30) as u32, + 1 => [0, 31][r.below(2) as usize], + _ => 12, + }; + let odd = r.below(32) == 0; + let header = |r: &mut Rng| { + let gf = if r.below(32) == 0 { 128 + r.below(64) as u32 } else { 16 }; + let shift = if r.below(32) == 0 { 128 + r.below(64) as u32 } else { 8 + r.below(16) as u32 }; + (gf, shift) + }; + let cz = 1 + r.below(3) as u32; + let cy = [1u32, 2, 2, 4][r.below(4) as usize]; + let divisors: Vec = [1u32, 2, 4].into_iter().filter(|d| cy % d == 0).collect(); + let kk = if r.below(32) == 0 { 3 } else { divisors[r.below(divisors.len() as u64) as usize] }; + // h_s: [Cz, Zh, Zw] → [2·Cy, Yh, Yw]. + let (n, relu_only) = (r.below(2) as usize, r.below(32) == 0); + let (mut hl, c) = splat_layers(r, cz, n, relu_only); + let out = if r.below(16) == 0 { 1 + r.below(6) as u32 } else { 2 * cy }; + hl.push(last_layer(r, c, out)); + let (gf, shift) = header(r); + let h_s = whm1_bytes(if odd { afrac ^ 1 } else { afrac }, gf, shift, &hl); + // entropy: [3·Cy, Yh, Yw] → [2·Cy, Yh, Yw], keeping the size. + let mut el = Vec::new(); + let mut c = 3 * cy; + match r.below(8) { + 0 | 1 => el.push(Whm::Leaky { q: if r.below(32) == 0 { r.signed(1 << 40) } else { r.signed(1 << 10) }, f: if r.below(32) == 0 { 64 } else { r.below(16) as u32 } }), + 2 => el.push(Whm::Relu), + 3 => { + let g: Vec = (0..c * c).map(|_| r.below(1 << 12) as i32).collect(); + let b: Vec = (0..c).map(|_| r.below(1 << 28) as i64).collect(); + el.push(Whm::Igdn { c, g, b }); + } + 4 | 5 => { + let (l, c2) = splat_layers(r, c, 1, false); + el.extend(l); + c = c2; + } + _ => {} + } + if r.below(2) == 0 { + let mid = 1 + r.below(6) as u32; + el.push(same_size_layer(r, c, mid)); + el.push(Whm::Leaky { q: r.signed(1 << 10), f: r.below(16) as u32 }); + c = mid; + } + let out = if r.below(16) == 0 { 1 + r.below(6) as u32 } else { 2 * cy }; + el.push(same_size_layer(r, c, out)); + let (gf, shift) = header(r); + let entropy = whm1_bytes(afrac, gf, shift, &el); + // g_s: [Cy, Yh, Yw] → [3, H, W]. + let (n, relu_only) = (r.below(3) as usize, r.below(32) == 0); + let (mut gl, c) = splat_layers(r, cy, n, relu_only); + let out = if r.below(16) == 0 { 1 + r.below(4) as u32 } else { 3 }; + gl.push(last_layer(r, c, out)); + let (gf, shift) = header(r); + let g_s = whm1_bytes(afrac, gf, shift, &gl); + + // The tables. + let a = afrac.min(30); + let n_edges = if r.below(32) == 0 { 0 } else { 1 + r.below(4) as usize }; + let mut edges = Vec::new(); + let mut e = 1 + r.below(1 << (a + 2)) as i64; + for _ in 0..n_edges { + edges.push(e); + e += 1 + r.below(1 << (a + 2)) as i64; + } + let cdfs: Vec> = (0..n_edges).map(|_| cdf_table(r)).collect(); + let mut cdf_lengths: Vec = cdfs.iter().map(Vec::len).collect(); + let offsets: Vec = (0..n_edges).map(|_| r.signed(3) as i32).collect(); + let k = if r.below(32) == 0 { cz as usize - 1 } else { cz as usize }; + let z_cdfs: Vec> = (0..k).map(|_| cdf_table(r)).collect(); + let mut z_cdf_lengths: Vec = z_cdfs.iter().map(Vec::len).collect(); + if r.below(32) == 0 { + match (n_edges, k) { + (1.., _) if r.below(2) == 0 => cdf_lengths[0] += 1, + (_, 1..) => z_cdf_lengths[0] += 1, + _ => {} + } + } + let z_offsets: Vec = (0..k).map(|_| r.signed(3) as i32).collect(); + let mut z_median_q: Vec = (0..k) + .map(|_| match r.below(8) { + 0 => [1i64 << 47, -(1 << 47), 1 << 60, -(1 << 60), i64::MAX, i64::MIN][r.below(6) as usize], + _ => r.signed(1 << (a + 4)), + }) + .collect(); + + // The hyper-latent. + let zh = 2 + r.below(2) as usize; + let zw = if r.below(16) == 0 { 1 + r.below(3) as usize } else { zh }; + let (nz, plane) = (cz as usize, zh * zw); + let zt = |ch: usize| ch.min(k.saturating_sub(1)); + let mut zv: Vec = Vec::with_capacity(nz * plane); + for ch in 0..nz { + let (off, span) = if k == 0 { (0, 1) } else { (z_offsets[zt(ch)] as i64, z_cdfs[zt(ch)].len() as i64 - 2) }; + for _ in 0..plane { + zv.push(match r.below(32) { + 0 => r.signed(1 << 20), + 1..=4 => off + span + r.below(40) as i64 - 20, + _ => off + r.below(span.max(1) as u64) as i64, + } as i32); + } + } + if r.below(4) == 0 && k > 0 { + if let Ok(g) = wai_ops::lower::whm1(&h_s, false) { + let x0 = g.tensors[0].hi as i128; + let ch = r.below(k as u64) as usize; + let v = z_offsets[ch]; + let target = [x0, x0 + 1, -x0, -x0 - 1][r.below(4) as usize]; + if let Ok(med) = i64::try_from(target - ((v as i128) << a)) { + z_median_q[ch] = med; + zv[ch * plane..(ch + 1) * plane].iter_mut().for_each(|x| *x = v); + } + } + } + let random = |r: &mut Rng| (0..8 + 4 * r.below(8)).map(|_| r.below(256) as u8).collect::>(); + let mut z_rans = if k == 0 { + random(r) + } else { + let vals: Vec<(i32, &[i32], usize, i32)> = (0..nz * plane).map(|i| { let t = zt(i / plane); (zv[i], z_cdfs[t].as_slice(), z_cdf_lengths[t], z_offsets[t]) }).collect(); + rans_of(&vals).unwrap_or_else(|| random(r)) + }; + + // The sizes, then the latent, coded in the decode's order against the direct networks. + let side = |l: &[Whm], n: usize| splat_side(l, n as i64).unwrap_or(1) as usize; + let (mut yh, yw) = (side(&hl, zh), side(&hl, zw)); + if r.below(8) == 0 { + yh += 1; + } + let (mut hh, ww) = (side(&gl, yh), side(&gl, yw)); + if r.below(8) == 0 { + hh += 1; + } + let (hm, em) = (IntModel::from_bytes(&h_s).unwrap(), IntModel::from_bytes(&entropy).unwrap()); + let cyu = cy as usize; + let ny = cyu * yh * yw; + let z_hat: Option> = (0..nz * plane).map(|i| (zv[i] as i64).checked_mul(1 << a)?.checked_add(*z_median_q.get(zt(i / plane))?)).collect(); + let hyper = z_hat.and_then(|z| { + let (o, oc, oh, ow) = run_net_checked(&hm, &z, nz, zh, zw).ok()?; + ((oc, oh, ow) == (2 * cyu, yh, yw)).then_some(o) + }); + let mut vals: Vec<(i32, Vec, usize, i32)> = Vec::new(); + if let (Some(hyper), 1..) = (hyper, n_edges) { + let mut ctx = vec![0i64; 3 * ny]; + ctx[ny..].copy_from_slice(&hyper); + let width = cyu / kk as usize; + 'all: for j in 0..kk as usize { + for anchor in [true, false] { + let Ok((params, ..)) = run_net_checked(&em, &ctx, 3 * cyu, yh, yw) else { break 'all }; + if params.len() != 2 * ny { + break 'all; + } + for ch in j * width..(j + 1) * width { + for (row, col) in (0..yh).flat_map(|y| (0..yw).map(move |x| (y, x))) { + if ((row + col) % 2 == 0) != anchor { + continue; + } + let e = ch * yh * yw + row * yw + col; + let b = wai::int_entropy::det_build_index(params[e], &edges); + let span = cdfs[b].len() as i64 - 2; + let v = match r.below(32) { + 0 => r.signed(1 << 20), + 1..=3 => offsets[b] as i64 + span + r.below(40) as i64 - 20, + _ => offsets[b] as i64 + r.below(span.max(1) as u64) as i64, + } as i32; + let Some(y) = wai::int_entropy::dequantize(v, a, params[ny + e]) else { break 'all }; + vals.push((v, cdfs[b].clone(), cdf_lengths[b], offsets[b])); + ctx[e] = y; + } + } + } + } + } + let refs: Vec<(i32, &[i32], usize, i32)> = vals.iter().map(|(v, t, l, o)| (*v, t.as_slice(), *l, *o)).collect(); + let mut y_rans = if refs.is_empty() { random(r) } else { rans_of(&refs).unwrap_or_else(|| random(r)) }; + damage(r, &mut z_rans); + damage(r, &mut y_rans); + let tables = json!({ + "afrac": afrac, "scale_edges": edges, "cdfs": cdfs, "cdf_lengths": cdf_lengths, "offsets": offsets, + "z_cdfs": z_cdfs, "z_cdf_lengths": z_cdf_lengths, "z_offsets": z_offsets, "z_median_q": z_median_q, + }); + let bundle = wmb1_bytes(&h_s, &entropy, &g_s, &tables); + let dims = [cz as u16, zh as u16, zw as u16, cy as u16, yh as u16, yw as u16, kk as u16]; + (bundle, wim1_bytes(hh as u32, ww as u32, dims, &z_rans, &y_rans)) +} + +/// The direct `int_mlicpp` decode, a panic (an overflow-checked build's arithmetic overflow) +/// caught. +fn mlic_direct(m: &wai::int_mlicpp::IntMlicModel, p: &[u8]) -> Option { + catch_unwind(AssertUnwindSafe(|| mlic_outcome(wai::int_mlicpp::legacy::decode_traced(m, p)))).ok() +} + +/// Gate 4 of the migration, for `wai.neural.int_mlicpp`: the decode through the operator +/// set against the direct decode on random bundles and payloads. Outside the planned +/// breaks both read the same `y` symbols, decode to the same image or both refuse with the +/// same code at the same check (a network's ranges, or the decode's own), and the direct +/// decode never panics; under a planned break the decode through the operator set refuses +/// (`model_mismatch`). A bundle that does not parse is refused by both alike before +/// either decode starts. +#[test] +fn int_mlicpp_random_sets_and_payloads_decode_alike() { + use std::collections::BTreeMap; + let (mut alike, mut unparsed, mut refused, mut breaks) = (0u64, 0u64, BTreeMap::::new(), BTreeMap::<&str, u64>::new()); + let n = cases("int_mlicpp", 3000, |r, seed| { + let (bundle, p) = int_mlicpp_case(r); + let why = || format!("seed {seed}\nbundle {bundle:?}\npayload {p:?}"); + let Ok(m) = wai::int_mlicpp::IntMlicModel::from_bundle(&bundle) else { + unparsed += 1; + return; + }; + let new = mlic_outcome(wai::int_mlicpp::ops::decode_traced(&m, &p)); + match int_mlicpp_planned_break(&bundle, &p) { + Some(b) => { + assert!(matches!(&new.2, Err(e) if e.starts_with("model_mismatch")), "{b}: the decode through the operator set must refuse it: {:?}\n{}", new.2, why()); + *breaks.entry(b).or_default() += 1; + } + None => match mlic_direct(&m, &p) { + Some(old) => { + assert!(new == old, "the two decodes differ: {:?} / {:?}\n{}", new.2.as_ref().err(), old.2.as_ref().err(), why()); + match &new.2 { + Ok(_) => alike += 1, + Err(e) => { + let (code, msg) = e.split_once(": ").unwrap_or((e, "")); + let kind = format!("{code}: {}", msg.split_whitespace().take(2).collect::>().join(" ")); + *refused.entry(kind).or_default() += 1; + } + } + } + None => panic!("the direct decode panicked\n{}", why()), + }, + } + }); + eprintln!("int_mlicpp: {alike} decoded alike, {unparsed} bundles refused by both before decoding, refused by both {refused:?}, planned breaks {breaks:?}"); + // Three networks, two streams and a bundle compound the generator's refusal rates. + assert!(alike * 10 > n, "the generator must produce decodable cases: {alike} of {n}"); + let reached = |k: &str| refused.keys().any(|x| x.contains(k)); + assert!(n < 1000 || ["h_s", "entropy", "g_s", "stream", "model_mismatch", "payload_malformed"].iter().all(|k| reached(k)), "a refusal was not reached: {refused:?}"); + assert!(n < 1000 || breaks.len() >= 3, "too few kinds of planned break reached: {breaks:?}"); +} + +/// The planned breaks of integer-operators §15.3 for `wai.neural.int_mlicpp`, each on a +/// bundle and payload the direct decode decodes, or refuses later or with another code: +/// the classifier names it, and the decode through the operator set refuses it +/// (`model_mismatch`, before either stream). +#[test] +fn int_mlicpp_planned_breaks_are_new_refusals() { + use wai::int_mlicpp::{legacy, ops, IntMlicModel}; + // Cy = 1, K = 1, a latent of side 2; Cz = 2, a hyper-latent of side 2; an image of + // side 2. h_s: the hyper-latent's two channels as they are; entropy: σ and μ from them; + // g_s: the latent to each of three channels. Every z symbol is the first of its table + // (offset 2), so every hyper-latent is 2·2^12; every y symbol the first of the one + // scale edge's table (offset 2), so every latent is 2·2^12 + 2·2^12, clamped to 2^12: + // 255. + let t3 = vec![0, 1 << 15, 1 << 16]; + let tables = |z_short: bool| { + let nz = if z_short { 1 } else { 2 }; + json!({ + "afrac": 12, "scale_edges": [1 << 12], "cdfs": [t3], "cdf_lengths": [3], "offsets": [2], + "z_cdfs": vec![t3.clone(); nz], "z_cdf_lengths": vec![3; nz], "z_offsets": vec![2; nz], "z_median_q": vec![0; nz], + }) + }; + let zs: Vec<(i32, &[i32], usize, i32)> = (0..8).map(|_| (2, t3.as_slice(), 3, 2)).collect(); + let ys: Vec<(i32, &[i32], usize, i32)> = (0..4).map(|_| (2, t3.as_slice(), 3, 2)).collect(); + let (z, y) = (rans_of(&zs).unwrap(), rans_of(&ys).unwrap()); + let payload = |z: &[u8], y: &[u8]| wim1_bytes(2, 2, [2, 2, 2, 1, 2, 2, 1], z, y); + let ok = payload(&z, &y); + let cut = |s: &[u8]| s[..s.len() - 4].to_vec(); + let ident = Whm::Conv { inc: 2, out: 2, k: 1, s: 1, p: 0, w: vec![1, 0, 0, 1], m: vec![1; 2], b: vec![0; 2] }; + let pick = Whm::Conv { inc: 3, out: 2, k: 1, s: 1, p: 0, w: vec![0, 1, 0, 0, 0, 1], m: vec![1; 2], b: vec![0; 2] }; + let to3 = |w: i16, b: i64| Whm::ConvT { inc: 1, out: 3, k: 1, s: 1, p: 0, op: 0, w: vec![w; 3], m: vec![1; 3], b: vec![b; 3] }; + let net = |shift: u32, gf: u32, l: Vec| whm1_bytes(12, gf, shift, &l); + let (h0, e0, g0) = (net(0, 16, vec![ident.clone()]), net(0, 16, vec![pick.clone()]), net(0, 16, vec![to3(1, 0)])); + let b = |h: &[u8], e: &[u8], g: &[u8]| wmb1_bytes(h, e, g, &tables(false)); + let decode = |bundle: &[u8], p: &[u8]| { + let m = IntMlicModel::from_bundle(bundle).unwrap(); + let new = ops::decode(&m, p).map(|i| i.rgb).map_err(|e| mlic_label(&e)); + let old = catch_unwind(AssertUnwindSafe(|| legacy::decode(&m, p).map(|i| i.rgb).map_err(|e| mlic_label(&e)))).ok(); + (new, old) + }; + // The base decodes alike. + assert_eq!(int_mlicpp_planned_break(&b(&h0, &e0, &g0), &ok), None); + let (new, old) = decode(&b(&h0, &e0, &g0), &ok); + assert_eq!((new, old), (Ok(vec![255; 12]), Some(Ok(vec![255; 12])))); + let expect = |kind: &str, bundle: Vec, p: &[u8], direct: Option<&str>| { + assert_eq!(int_mlicpp_planned_break(&bundle, p), Some(kind), "{kind}: the classifier"); + let (new, old) = decode(&bundle, p); + match direct { + None => assert!(matches!(old, Some(Ok(_))), "{kind}: the direct decode decodes it: {old:?}"), + Some(code) => assert!(matches!(&old, Some(Err(e)) if e.starts_with(code)), "{kind}: the direct decode's code: {old:?}"), + } + assert!(matches!(&new, Err(e) if e.starts_with("model_mismatch")), "{kind}: the decode through the operator set refuses it before the streams: {new:?}"); + }; + // The breaks of the other WHM1 decoders, in each of the three networks. + expect("no layer of tag 0, 1 or 2", b(&net(0, 16, vec![Whm::Relu]), &e0, &g0), &ok, None); + let igdn = Whm::Igdn { c: 2, g: vec![0; 4], b: vec![1 << 24; 2] }; + expect("a shift or gf of 128 or more that no layer uses", b(&net(200, 16, vec![igdn]), &e0, &g0), &ok, None); + expect("a shift or gf of 128 or more that no layer uses", b(&h0, &e0, &net(0, 200, vec![to3(1, 0)])), &ok, None); + expect("a bias of magnitude 2^63 - 1 or more", b(&h0, &e0, &net(0, 16, vec![to3(1, i64::MAX)])), &ok, Some("payload_malformed: g_s")); + let first = Whm::ConvT { inc: 1, out: 1, k: 1, s: 1, p: 0, op: 0, w: vec![0], m: vec![1], b: vec![1 << 30] }; + let tight = Whm::ConvT { inc: 1, out: 3, k: 1, s: 1, p: 0, op: 0, w: vec![1 << 11; 3], m: vec![1 << 40; 3], b: vec![0; 3] }; + expect("a check range its producer cannot meet", b(&h0, &e0, &net(0, 16, vec![first, tight])), &ok, Some("payload_malformed: g_s")); + // The rules the direct decode makes only when a network runs: the same code where the + // streams are whole, and the stream's where the streams run out first. + let chain = |inc: u32| vec![to3(1, 0), Whm::ConvT { inc, out: 3, k: 1, s: 1, p: 0, op: 0, w: vec![1; inc as usize * 3], m: vec![1; 3], b: vec![0; 3] }]; + expect("a layer that does not fit the channels before it", b(&h0, &e0, &net(0, 16, chain(2))), &ok, Some("model_mismatch: g_s")); + expect("a layer that does not fit the channels before it", b(&h0, &e0, &net(0, 16, chain(2))), &payload(&z, &cut(&y)), Some("payload_malformed: y stream")); + expect("a shift or gf of 128 or more that a layer uses", b(&h0, &e0, &net(200, 16, vec![to3(1, 0)])), &ok, Some("model_mismatch: g_s")); + expect("a shift or gf of 128 or more that a layer uses", b(&h0, &e0, &net(200, 16, vec![to3(1, 0)])), &payload(&z, &cut(&y)), Some("payload_malformed: y stream")); + let leaky64 = net(0, 16, vec![pick.clone(), Whm::Leaky { q: 1, f: 64 }]); + expect("a leaky ReLU's shift of 64 or more", b(&h0, &leaky64, &g0), &ok, Some("model_mismatch: entropy")); + expect("a leaky ReLU's shift of 64 or more", b(&h0, &leaky64, &g0), &payload(&cut(&z), &y), Some("payload_malformed: z stream")); + let reads3 = net(0, 16, vec![Whm::Conv { inc: 3, out: 2, k: 1, s: 1, p: 0, w: vec![1; 6], m: vec![1; 2], b: vec![0; 2] }]); + expect("a network whose input is not the payload's channel count", b(&reads3, &e0, &g0), &ok, Some("model_mismatch: h_s")); + expect("a network whose input is not the payload's channel count", b(&reads3, &e0, &g0), &payload(&cut(&z), &y), Some("payload_malformed: z stream")); + // A convolution padded by 2^26 a side and strided by 2^27: the direct decode's geometry + // gives the output, of side 2, and decodes; the operator set's run-time plan caps the + // padded side. + let far = Whm::Conv { inc: 1, out: 1, k: 1, s: 1 << 27, p: 1 << 26, w: vec![1], m: vec![1], b: vec![0] }; + expect("a convolution's padded side past 2^26", b(&h0, &e0, &net(0, 16, vec![far, to3(1, 0)])), &ok, None); + // Not breaks: a bundle with too few z tables, and a network the shapes refuse, are + // refused alike before either decode reads a stream. + let short = wmb1_bytes(&h0, &e0, &g0, &tables(true)); + assert_eq!(int_mlicpp_planned_break(&short, &ok), None); + let (new, old) = decode(&short, &ok); + assert!(matches!((&new, &old), (Err(a), Some(Err(b))) if a == b && a.starts_with("model_mismatch")), "{new:?} {old:?}"); +} + // ------------------------------------------------------------------ load step 8 /// What the bounds pass made of the graphs compared: refused, or the profiles met. @@ -1434,6 +1928,14 @@ fn the_bounds_pass_equals_its_oracle_on_random_sets() { same_pass(&g, &mut v, &why); } } + let (bundle, _) = int_mlicpp_case(r); + if let Some(secs) = wmb1_sections(&bundle) { + for (k, rgb8) in [(0, false), (1, false), (2, true)] { + if let Ok(g) = wai_ops::lower::whm1(&secs[k], rgb8) { + same_pass(&g, &mut v, &why); + } + } + } }); eprintln!("bounds pass: {n} cases, {v:?}"); assert!(v.graphs > n * 2, "the generators must mostly lower: {} graphs from {n} cases", v.graphs); -- 2.54.0 (Apple Git-157) From eb174546ad656c39a08306d574a2c40eb7b1b73e Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:04:44 -0400 Subject: [PATCH 17/22] int_mlicpp gate 4: the generator perturbs less often, so more cases decode The first run (3000 cases, seed 3372) found no disagreement, but only 94 cases decoded: the bundle, header and shape perturbations each refused a share of cases alike before any stream. They are now one in 32 or 64, and the entropy network's optional layers all keep the size. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/int_ops_differential.rs | 71 ++++++++++++------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/wai/wai-rs/tests/int_ops_differential.rs b/wai/wai-rs/tests/int_ops_differential.rs index fe8325c5f..ebaf7005c 100644 --- a/wai/wai-rs/tests/int_ops_differential.rs +++ b/wai/wai-rs/tests/int_ops_differential.rs @@ -1501,7 +1501,7 @@ fn mlic_outcome((t, r): (wai::int_mlicpp::Trace, Result Whm { let k = [1u32, 3][r.below(2) as usize]; - let p = if r.below(16) == 0 { r.below(3) as u32 } else { k / 2 }; + let p = if r.below(32) == 0 { r.below(3) as u32 } else { k / 2 }; let wmax = if r.below(8) == 0 { 32767 } else { [16, 1024, 4096][r.below(3) as usize] }; let w: Vec = (0..c * out * k * k).map(|_| if r.below(4) == 0 { 0 } else { r.signed(wmax) as i16 }).collect(); let m: Vec = (0..out).map(|_| if r.below(32) == 0 { r.signed(1 << 40) } else { r.signed(1 << 12) }).collect(); @@ -1516,43 +1516,44 @@ fn same_size_layer(r: &mut Rng, c: u32, out: u32) -> Whm { } /// A random `wai.neural.int_mlicpp` bundle and `WIM1` payload. The bundle: `afrac` 12 -/// mostly (one in sixteen anywhere in 1..=30, one in sixteen at 0 or 31, and one network's -/// not the others' one in 32); each network's header `gf` or `shift` of 128 or more one in -/// 32 each; `h_s` from `Cz` (1 to 3) channels, up to one of `splat_layers`' layers then a -/// last layer to `2·Cy` (one in sixteen another count); `entropy` from `3·Cy`, up to one -/// layer that keeps the size (a leaky ReLU, a ReLU or an inverse GDN, or one in four -/// `splat_layers`' draw), then [`same_size_layer`] to `2·Cy` (one in sixteen another -/// count); `g_s` from `Cy` (1, 2 or 4), up to two layers then a last layer to three -/// channels (one in sixteen not); the tables as [`int_hyper_case`]'s, with `afrac`. The -/// payload: `K` a divisor of `Cy` (one in 32 not); a hyper-latent of side 2 or 3 (one in -/// sixteen not square), its symbols as [`int_hyper_case`]'s, one case in four with a whole -/// channel at `h_s`'s input `check` range or one past it; the latent's size `h_s`'s output -/// and the image's `g_s`'s (each one in eight off by one); the `y` stream coded in the -/// decode's order (each slice's anchors, then the rest), each symbol against the table the -/// direct networks' `σ` buckets it to, mostly inside its table, some escaped and some far, -/// until a network refuses (the rest of the stream is then random); each stream damaged as -/// [`damage`] says. +/// mostly (one in sixteen anywhere in 1..=30, one in 32 at 0 or 31, and one network's not +/// the others' one in 64); each network's header `gf` or `shift` of 128 or more one in 64 +/// each; `h_s` from `Cz` (1 to 3) channels, up to one of `splat_layers`' layers then a last +/// layer to `2·Cy` (one in 32 another count); `entropy` from `3·Cy`, up to one layer that +/// keeps the size (a leaky ReLU, a ReLU, an inverse GDN, or [`same_size_layer`]) and up to +/// one convolution and leaky ReLU more, then [`same_size_layer`] to `2·Cy` (one in 32 +/// another count); `g_s` from `Cy` (1, 2 or 4), up to two layers then a last layer to three +/// channels (one in 32 not); the tables as [`int_hyper_case`]'s, with `afrac` (one set in +/// 64 with no scale edge, a hyper-latent channel short, or a table's length past its row). +/// The payload: `K` a divisor of `Cy` (one in 64 not); a hyper-latent of side 2 or 3 (one +/// in sixteen not square), its symbols as [`int_hyper_case`]'s, one case in four with a +/// whole channel at `h_s`'s input `check` range or one past it; the latent's size `h_s`'s +/// output and the image's `g_s`'s (each one in sixteen off by one); the `y` stream coded in +/// the decode's order (each slice's anchors, then the rest), each symbol against the table +/// the direct networks' `σ` buckets it to, mostly inside its table, some escaped and some +/// far, until a network refuses (the rest of the stream is then random); each stream +/// damaged as [`damage`] says. fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { use wai::int_hyper_synth::run_net_checked; - let afrac = match r.below(16) { - 0 => 1 + r.below(30) as u32, - 1 => [0, 31][r.below(2) as usize], + let afrac = match r.below(32) { + 0 | 1 => 1 + r.below(30) as u32, + 2 => [0, 31][r.below(2) as usize], _ => 12, }; - let odd = r.below(32) == 0; + let odd = r.below(64) == 0; let header = |r: &mut Rng| { - let gf = if r.below(32) == 0 { 128 + r.below(64) as u32 } else { 16 }; - let shift = if r.below(32) == 0 { 128 + r.below(64) as u32 } else { 8 + r.below(16) as u32 }; + let gf = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 16 }; + let shift = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 8 + r.below(16) as u32 }; (gf, shift) }; let cz = 1 + r.below(3) as u32; let cy = [1u32, 2, 2, 4][r.below(4) as usize]; let divisors: Vec = [1u32, 2, 4].into_iter().filter(|d| cy % d == 0).collect(); - let kk = if r.below(32) == 0 { 3 } else { divisors[r.below(divisors.len() as u64) as usize] }; + let kk = if r.below(64) == 0 { 3 } else { divisors[r.below(divisors.len() as u64) as usize] }; // h_s: [Cz, Zh, Zw] → [2·Cy, Yh, Yw]. let (n, relu_only) = (r.below(2) as usize, r.below(32) == 0); let (mut hl, c) = splat_layers(r, cz, n, relu_only); - let out = if r.below(16) == 0 { 1 + r.below(6) as u32 } else { 2 * cy }; + let out = if r.below(32) == 0 { 1 + r.below(6) as u32 } else { 2 * cy }; hl.push(last_layer(r, c, out)); let (gf, shift) = header(r); let h_s = whm1_bytes(if odd { afrac ^ 1 } else { afrac }, gf, shift, &hl); @@ -1568,9 +1569,9 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { el.push(Whm::Igdn { c, g, b }); } 4 | 5 => { - let (l, c2) = splat_layers(r, c, 1, false); - el.extend(l); - c = c2; + let mid = 1 + r.below(6) as u32; + el.push(same_size_layer(r, c, mid)); + c = mid; } _ => {} } @@ -1580,21 +1581,21 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { el.push(Whm::Leaky { q: r.signed(1 << 10), f: r.below(16) as u32 }); c = mid; } - let out = if r.below(16) == 0 { 1 + r.below(6) as u32 } else { 2 * cy }; + let out = if r.below(32) == 0 { 1 + r.below(6) as u32 } else { 2 * cy }; el.push(same_size_layer(r, c, out)); let (gf, shift) = header(r); let entropy = whm1_bytes(afrac, gf, shift, &el); // g_s: [Cy, Yh, Yw] → [3, H, W]. let (n, relu_only) = (r.below(3) as usize, r.below(32) == 0); let (mut gl, c) = splat_layers(r, cy, n, relu_only); - let out = if r.below(16) == 0 { 1 + r.below(4) as u32 } else { 3 }; + let out = if r.below(32) == 0 { 1 + r.below(4) as u32 } else { 3 }; gl.push(last_layer(r, c, out)); let (gf, shift) = header(r); let g_s = whm1_bytes(afrac, gf, shift, &gl); // The tables. let a = afrac.min(30); - let n_edges = if r.below(32) == 0 { 0 } else { 1 + r.below(4) as usize }; + let n_edges = if r.below(64) == 0 { 0 } else { 1 + r.below(4) as usize }; let mut edges = Vec::new(); let mut e = 1 + r.below(1 << (a + 2)) as i64; for _ in 0..n_edges { @@ -1604,10 +1605,10 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { let cdfs: Vec> = (0..n_edges).map(|_| cdf_table(r)).collect(); let mut cdf_lengths: Vec = cdfs.iter().map(Vec::len).collect(); let offsets: Vec = (0..n_edges).map(|_| r.signed(3) as i32).collect(); - let k = if r.below(32) == 0 { cz as usize - 1 } else { cz as usize }; + let k = if r.below(64) == 0 { cz as usize - 1 } else { cz as usize }; let z_cdfs: Vec> = (0..k).map(|_| cdf_table(r)).collect(); let mut z_cdf_lengths: Vec = z_cdfs.iter().map(Vec::len).collect(); - if r.below(32) == 0 { + if r.below(64) == 0 { match (n_edges, k) { (1.., _) if r.below(2) == 0 => cdf_lengths[0] += 1, (_, 1..) => z_cdf_lengths[0] += 1, @@ -1661,11 +1662,11 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { // The sizes, then the latent, coded in the decode's order against the direct networks. let side = |l: &[Whm], n: usize| splat_side(l, n as i64).unwrap_or(1) as usize; let (mut yh, yw) = (side(&hl, zh), side(&hl, zw)); - if r.below(8) == 0 { + if r.below(16) == 0 { yh += 1; } let (mut hh, ww) = (side(&gl, yh), side(&gl, yw)); - if r.below(8) == 0 { + if r.below(16) == 0 { hh += 1; } let (hm, em) = (IntModel::from_bytes(&h_s).unwrap(), IntModel::from_bytes(&entropy).unwrap()); -- 2.54.0 (Apple Git-157) From 3e40a9f7991def15fb97186f228f4bcc1ca7f212 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:05:43 -0400 Subject: [PATCH 18/22] int_mlicpp gate 4: the generator keeps more values inside the networks' ranges Second run (3000 cases): 170 decoded, no disagreement. The refusals were mostly hyper-latents placed at h_s's range, extreme medians and values past the entropy network's range. Now: a channel at h_s's range one case in eight, medians at an extreme one in sixteen, header shifts 12 to 23, and the entropy layers' weights, multipliers and extreme biases drawn smaller or rarer. The shared layer generators are unchanged. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/int_ops_differential.rs | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/wai/wai-rs/tests/int_ops_differential.rs b/wai/wai-rs/tests/int_ops_differential.rs index ebaf7005c..66e801178 100644 --- a/wai/wai-rs/tests/int_ops_differential.rs +++ b/wai/wai-rs/tests/int_ops_differential.rs @@ -1496,17 +1496,18 @@ fn mlic_outcome((t, r): (wai::int_mlicpp::Trace, Result Whm { let k = [1u32, 3][r.below(2) as usize]; let p = if r.below(32) == 0 { r.below(3) as u32 } else { k / 2 }; - let wmax = if r.below(8) == 0 { 32767 } else { [16, 1024, 4096][r.below(3) as usize] }; + let wmax = if r.below(16) == 0 { 32767 } else { [16, 256, 1024][r.below(3) as usize] }; let w: Vec = (0..c * out * k * k).map(|_| if r.below(4) == 0 { 0 } else { r.signed(wmax) as i16 }).collect(); - let m: Vec = (0..out).map(|_| if r.below(32) == 0 { r.signed(1 << 40) } else { r.signed(1 << 12) }).collect(); + let m: Vec = (0..out).map(|_| if r.below(128) == 0 { r.signed(1 << 40) } else { r.signed(1 << 12) }).collect(); let b: Vec = (0..out) - .map(|_| match r.below(64) { + .map(|_| match r.below(256) { 0 => [i64::MAX, i64::MIN, i64::MIN + 1, i64::MAX - 1][r.below(4) as usize], 1 => r.signed(1 << 40), _ => r.signed(1 << 20), @@ -1517,8 +1518,8 @@ fn same_size_layer(r: &mut Rng, c: u32, out: u32) -> Whm { /// A random `wai.neural.int_mlicpp` bundle and `WIM1` payload. The bundle: `afrac` 12 /// mostly (one in sixteen anywhere in 1..=30, one in 32 at 0 or 31, and one network's not -/// the others' one in 64); each network's header `gf` or `shift` of 128 or more one in 64 -/// each; `h_s` from `Cz` (1 to 3) channels, up to one of `splat_layers`' layers then a last +/// the others' one in 64); each network's header `shift` 12 to 23, and its `gf` or `shift` +/// of 128 or more one in 64 each; `h_s` from `Cz` (1 to 3) channels, up to one of `splat_layers`' layers then a last /// layer to `2·Cy` (one in 32 another count); `entropy` from `3·Cy`, up to one layer that /// keeps the size (a leaky ReLU, a ReLU, an inverse GDN, or [`same_size_layer`]) and up to /// one convolution and leaky ReLU more, then [`same_size_layer`] to `2·Cy` (one in 32 @@ -1526,8 +1527,9 @@ fn same_size_layer(r: &mut Rng, c: u32, out: u32) -> Whm { /// channels (one in 32 not); the tables as [`int_hyper_case`]'s, with `afrac` (one set in /// 64 with no scale edge, a hyper-latent channel short, or a table's length past its row). /// The payload: `K` a divisor of `Cy` (one in 64 not); a hyper-latent of side 2 or 3 (one -/// in sixteen not square), its symbols as [`int_hyper_case`]'s, one case in four with a -/// whole channel at `h_s`'s input `check` range or one past it; the latent's size `h_s`'s +/// in sixteen not square), its symbols as [`int_hyper_case`]'s (medians at an extreme one +/// in sixteen), one case in eight with a whole channel at `h_s`'s input `check` range or +/// one past it; the latent's size `h_s`'s /// output and the image's `g_s`'s (each one in sixteen off by one); the `y` stream coded in /// the decode's order (each slice's anchors, then the rest), each symbol against the table /// the direct networks' `σ` buckets it to, mostly inside its table, some escaped and some @@ -1543,7 +1545,7 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { let odd = r.below(64) == 0; let header = |r: &mut Rng| { let gf = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 16 }; - let shift = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 8 + r.below(16) as u32 }; + let shift = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 12 + r.below(12) as u32 }; (gf, shift) }; let cz = 1 + r.below(3) as u32; @@ -1617,7 +1619,7 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { } let z_offsets: Vec = (0..k).map(|_| r.signed(3) as i32).collect(); let mut z_median_q: Vec = (0..k) - .map(|_| match r.below(8) { + .map(|_| match r.below(16) { 0 => [1i64 << 47, -(1 << 47), 1 << 60, -(1 << 60), i64::MAX, i64::MIN][r.below(6) as usize], _ => r.signed(1 << (a + 4)), }) @@ -1639,7 +1641,7 @@ fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { } as i32); } } - if r.below(4) == 0 && k > 0 { + if r.below(8) == 0 && k > 0 { if let Ok(g) = wai_ops::lower::whm1(&h_s, false) { let x0 = g.tensors[0].hi as i128; let ch = r.below(k as u64) as usize; -- 2.54.0 (Apple Git-157) From fad441326993abc96f267833273d87a6cbfeb75d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:09:19 -0400 Subject: [PATCH 19/22] int_mlicpp gate 5 (WIP: committed before running): the fast kernels behind the interpreter, held to it int_ops::equivalence::the_mlicpp_networks_run_on_the_fast_kernels: the committed vector through wmb1/1 under every fast policy runs every convolution and transposed convolution of its three graphs, each time it runs (the entropy network before each of the 2K passes), and g_s's inverse GDNs on the fast kernels (counted), with the interpreter's buckets, symbols and bytes. tests/int_mlicpp_ops.rs every_kernel_policy_...: reference and fast(1|2|5|0) alike on both envelopes, a y stream cut short, and hyper-latent channel 0 at h_s's range and one past it. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/int_ops.rs | 36 +++++++++++++++++++++++ wai/wai-rs/tests/int_mlicpp_ops.rs | 46 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/wai/wai-rs/src/int_ops.rs b/wai/wai-rs/src/int_ops.rs index 6b91dce7b..94f346621 100644 --- a/wai/wai-rs/src/int_ops.rs +++ b/wai/wai-rs/src/int_ops.rs @@ -413,6 +413,42 @@ mod equivalence { } } + /// The committed `wai.neural.int_mlicpp` vector through its lowering `wmb1/1`: under every + /// fast policy every convolution and transposed convolution of its three graphs, each + /// time it runs (`h_s` once, the entropy network before each of the `2·K` passes, `g_s` + /// once), and `g_s`'s inverse GDNs run on the fast kernels, and every bucket, symbol and + /// byte is the interpreter's. The leaky ReLU, the ReLU and the RGB8 step run on the + /// interpreter under every policy. + #[test] + fn the_mlicpp_networks_run_on_the_fast_kernels() { + use crate::int_kernels::testkit::fast_igdn_runs; + use crate::int_mlicpp::{ops, IntMlicModel}; + use wai_ops::graph::opcode; + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../wai-rs/tests/vectors/int_mlicpp"); + let m = IntMlicModel::from_bundle(&std::fs::read(dir.join("model.wmb")).unwrap()).unwrap(); + // The payload of the WAI1 envelope: magic, the manifest after its u32 length, then + // the payload after its own. + let env = std::fs::read(dir.join("stream.wai")).unwrap(); + let ml = u32::from_le_bytes(env[4..8].try_into().unwrap()) as usize; + let p = &env[12 + ml..]; + let k = u16::from_le_bytes([p[24], p[25]]) as u64; + let nets = ops::load(&m).unwrap(); + let count = |g: &wai_ops::Loaded, codes: &[u8]| g.graph().ops.iter().filter(|o| codes.contains(&o.code)).count() as u64; + let convs = [opcode::CONV, opcode::CONV_TRANSPOSE]; + let want_convs = count(&nets.h_s, &convs) + 2 * k * count(&nets.entropy, &convs) + count(&nets.g_s, &convs); + let want_igdns = count(&nets.h_s, &[opcode::IGDN]) + 2 * k * count(&nets.entropy, &[opcode::IGDN]) + count(&nets.g_s, &[opcode::IGDN]); + assert!(want_convs > 0 && want_igdns > 0, "the committed bundle has convolutions and inverse GDNs"); + let decode = |pol: Policy| fast_igdn_runs(|| fast_conv_runs(|| with_policy(pol, || ops::decode_traced(&m, p)))); + let ((want, none), no_igdn) = decode(Policy::reference()); + assert!(want.1.is_ok(), "the committed vector decodes"); + assert_eq!((none, no_igdn), (0, 0), "the reference policy runs no fast kernel"); + for pol in FAST { + let ((got, convs), igdns) = decode(pol); + assert!(got == want, "{pol:?}: the buckets, symbols or image differ from the interpreter alone"); + assert_eq!((convs, igdns), (want_convs, want_igdns), "{pol:?}: every convolution and inverse GDN runs on the fast kernels"); + } + } + /// The fast IGDN kernel behind the interpreter, against the interpreter alone, on random /// inverse GDNs: 1 to 6 channels, `gf` and `frac` across `0..=127` (the fast kernel takes /// those up to 62, the interpreter the rest), `γ` and `β` of either sign, and inputs whose diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs index 6df99e712..1041c7771 100644 --- a/wai/wai-rs/tests/int_mlicpp_ops.rs +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -463,3 +463,49 @@ fn every_check_range_refuses_exactly_what_the_direct_decode_refuses() { } assert_eq!(seen, 10, "every layer of the committed h_s, entropy and g_s that reads a check range"); } + +/// Gate 5 of the migration, the whole-decoder half: the decode through the lowering under +/// the reference kernels (the interpreter computes every operator) and under each fast +/// policy (the convolutions, the transposed convolutions and the inverse GDNs on this +/// crate's fast kernels, serial and threaded) reads the same buckets and symbols, and gives +/// the same image or the same refusal, on the committed vector (both envelopes), on a `y` +/// stream that runs out, and on bundles whose hyper-latent channel 0 lies about the edge +/// of `h_s`'s input `check` range (its median at `±X_0` and one past it). The per-operator +/// half is `int_ops`' equivalence tests, one of which counts the fast kernels' runs on this +/// bundle. +#[test] +fn every_kernel_policy_decodes_through_the_lowering_alike() { + 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)]; + let x0 = wai_ops::lower::whm1(§ions(&read(&format!("{DIR}/model.wmb")))[0], false).unwrap().tensors[0].hi; + let base = Wim1::committed(); + let z0 = base.z.clone(); + let mut cases: Vec<(String, IntMlicModel, Vec)> = vec![ + ("stream.wai".into(), model(), payload_of("stream.wai")), + ("stream_set.wai".into(), model(), payload_of("stream_set.wai")), + ("y cut by 8".into(), model(), wim1(|o| { let n = o.y.len() - 8; o.y.truncate(n); })), + ]; + // Channel 0's median moved to ±X_0 or one past it: its elements, the median plus each + // symbol at 2^12, lie about the edge of h_s's input check range. + let syms = { + let t = model(); + let mut d = wai::rans::RansDecoder::new(&z0).unwrap(); + let per = (base.dims[1] * base.dims[2]) as usize; + (0..per).map(|_| d.decode(&t.tables.z_cdfs[0], t.tables.z_cdf_lengths[0], t.tables.z_offsets[0])).collect::>() + }; + let (lo, hi) = (*syms.iter().min().unwrap() as i64, *syms.iter().max().unwrap() as i64); + for (what, med) in [("channel 0 at X_0", x0 - (hi << 12)), ("at X_0 + 1", x0 + 1 - (hi << 12)), ("at -X_0", -x0 - (lo << 12)), ("at -X_0 - 1", -x0 - 1 - (lo << 12))] { + cases.push((what.into(), bundle(move |m| m.tables.z_median_q[0] = med), base.bytes())); + } + let mut refused = 0; + for (k, (what, m, p)) in cases.iter().enumerate() { + let want = with_policy(POLICIES[0], || mlic::ops::decode_traced(m, p)); + assert!(k >= 2 || want.1.is_ok(), "{what}: the committed vector decodes"); + refused += want.1.is_err() as u32; + for pol in &POLICIES[1..] { + let got = with_policy(*pol, || mlic::ops::decode_traced(m, p)); + assert!(got == want, "{what}: {pol:?} differs from the interpreter alone"); + } + } + assert!(refused >= 3, "the cut stream and the two medians one past the edge are refused: {refused}"); +} -- 2.54.0 (Apple Git-157) From 1d6f14881e438b648e7fe92b3934b706e3702971 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:14:16 -0400 Subject: [PATCH 20/22] int_mlicpp gate 6 (WIP: committed before running): the switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit int_mlicpp::decode and decode_traced = ops::decode(_traced); int_mlicpp::legacy (the direct decode) behind cfg(any(test, feature = "legacy_int")), and run_net_checked and to_rgb8 imported only there. int_payload::decode_int's WIM1 branch: the bundle parsed, ops::check, the three networks from the store of loaded sets (key: capability, wmb1/1, acc64, the SHA-256 of model.wmb's listing) or ops::load, ops::fit, ops::decode_loaded. int_ops::WMB1; Resident for int_mlicpp::ops::Nets. Tests: int_mlicpp_ops the_new_load_rules_affect_no_committed_set_or_payload (every committed bundle meets the rules and loads; every committed WIM1 payload, in any file, plans, decodes or is refused alike); int_payload_cost (now also legacy_int): with its networks loaded the decode holds no more than the direct decode; through the sink the decode that finds the bundle held holds less than the one that loads it, and keeps nothing more; int_set_cache: the vector is kept and hit, one byte apart never shares an entry, a refused bundle is never kept. Text: integer-payloads §14 (the decode through wmb1/1, step 1's checks, and the codes the decoder reports: payload_malformed for the payload, its streams and its values, model_mismatch for a bundle that breaks its rules or does not fit); integer-operators §4, the status note and §15.3 item 3; SPEC §8 planned breaks; CHANGELOG. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 26 ++++++++ wai/SPEC.md | 9 +++ wai/extensions/integer-operators.md | 6 +- wai/extensions/integer-payloads.md | 22 ++++++- wai/wai-rs/Cargo.toml | 2 +- wai/wai-rs/src/int_mlicpp.rs | 46 +++++++++----- wai/wai-rs/src/int_ops.rs | 1 + wai/wai-rs/src/int_payload/mod.rs | 14 +++-- wai/wai-rs/src/int_set_cache.rs | 8 +++ wai/wai-rs/tests/int_mlicpp_ops.rs | 90 ++++++++++++++++++++++++++++ wai/wai-rs/tests/int_payload_cost.rs | 50 +++++++++++++++- wai/wai-rs/tests/int_set_cache.rs | 78 +++++++++++++++++++++--- 12 files changed, 318 insertions(+), 34 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 1b4bb4ace..cedb2ae65 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -16,6 +16,32 @@ wire format may change and such changes are recorded here. ([integer-payloads](extensions/integer-payloads.md) §14) and the lowering (integer-operators §13.4), and SPEC §7 names the capability among those that decode through the operator set. +- **The decode.** `int_mlicpp::decode`, and so `int_payload::decode_int`, the neural sink + and every sink that decodes through them, now runs the bundle's three networks through + their lowering `wmb1/1` ([integer-operators](extensions/integer-operators.md) §13.4): + each lowered by `whm1/1` and loaded under `acc64` with its certificate, then planned on + the payload's shapes, before either stream. The stage steps are unchanged, with the + `t2` table rule and the `final_only` end rule kept: a symbol decoded after a stream ran + out is still decoded and fed to the passes after it, and the refusal comes at the end. + The committed vector decodes to the same image, with the same bucket and symbol for + every element; every committed refusal keeps its code. The direct decode is kept, + test-only, behind the `legacy_int` feature. The store of loaded sets keeps the three + loaded networks under the SHA-256 of `model.wmb`. +- **Planned breaks** + ([integer-operators](extensions/integer-operators.md) §15.3 item 3, SPEC §8). A bundle + is refused before either stream (`model_mismatch`) for the rules of the other `WHM1` + decoders: a bias of magnitude `2^63 − 1` or more, a `check` range its producer cannot + meet, a network with no layer of tag 0, 1 or 2, a header `shift` or `gf` of 128 or more + that no layer uses. Four checks the decoder made only when a network ran now come + before the streams with the same code: a layer that does not fit the channels before + it, a network that does not read the payload's channel count, a header `shift` or `gf` + of 128 or more that a layer uses, and a leaky ReLU's shift of 64 or more. A convolution + whose padded side exceeds `2^26` is refused. +- **The refusal codes, written down.** + [integer-payloads](extensions/integer-payloads.md) §14 said that every refusal of this + capability is `payload_malformed`. The decoder reports `model_mismatch` for a bundle + that breaks its rules or does not fit the payload, as it did before this revision and + as its tests pin; §14 and integer-operators §4 now say so. ### Load step 8 in closed form diff --git a/wai/SPEC.md b/wai/SPEC.md index 583b91a8c..68c567aaf 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -2412,6 +2412,15 @@ envelope that uses any string a later revision registers. - `wai.neural.int_hyper` and the keyframe of `wai.video.int_hyper`: the same, for either of its networks, `h_s` and `g_s`: such a set is refused after the payload's shapes are checked and before either stream is decoded. + - `wai.neural.int_mlicpp`: the same, for any of its three networks, `h_s`, `entropy` + and `g_s`: such a bundle is refused after the payload's header and the networks' + geometry are checked and before either stream is decoded. Four checks its decoder + made when a network ran now come before the streams, with the same code + (`model_mismatch`): a layer that does not fit the channels before it, a network + that does not read the payload's channel count, a header `shift` or `gf` of 128 or + more that a layer uses, and a leaky ReLU's shift of 64 or more; where a stream or a + value was refused first, the bundle now is. A bundle with a convolution whose padded + side exceeds `2^26` is refused. --- diff --git a/wai/extensions/integer-operators.md b/wai/extensions/integer-operators.md index a7b2a2115..84f2fa381 100644 --- a/wai/extensions/integer-operators.md +++ b/wai/extensions/integer-operators.md @@ -14,7 +14,7 @@ > > **What a reader can build from this text.** With the payload extensions it cites, an implementation of this text decodes `wai.video.int_motion`, `wai.audio.int_codec`, `wai.splat.int_codec`, `wai.neural.int_synth`, `wai.neural.int_hyper`, `wai.video.int_hyper`, `wai.neural.int_mlicpp` and `wai.{image,video,audio,splat}.int_refine`, and reproduces the symbols and scale indices that `wai.image.jpegai` entropy-decodes (§13.6.1). The payload text four of these depend on, `entropy.json`, the `int_synth` payload and the `WIM1` payload with its `WMB1` bundle, is in [integer-payloads](integer-payloads.md), and §15.2 says where. The integer renderer of `wai.world.model` and the other integer networks of `wai.image.jpegai` are informative (§13.5, §13.6.2). > -> Reference implementation: the `wai-ops` crate (`wai/wai-ops`): parse, lowering, interpreter, stage operators and bounds. Tool: `wai-verify bounds`. Conformance: `wai/ops-conformance/`. A capability's reference decoder decodes through this operator set from the revision that registers the capability's `ops` member (§11); in this revision, `wai.neural.int_synth`, `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper` and `wai.video.int_hyper`. Until then, a capability's reference implementation is the decoder its payload extension names, and §15.3 lists where this text and that decoder differ. +> Reference implementation: the `wai-ops` crate (`wai/wai-ops`): parse, lowering, interpreter, stage operators and bounds. Tool: `wai-verify bounds`. Conformance: `wai/ops-conformance/`. A capability's reference decoder decodes through this operator set from the revision that registers the capability's `ops` member (§11); in this revision, `wai.neural.int_synth`, `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and `wai.neural.int_mlicpp`. Until then, a capability's reference implementation is the decoder its payload extension names, and §15.3 lists where this text and that decoder differ. > > The keywords **MUST**, **MUST NOT**, **SHOULD** and **MAY** are as defined in RFC 2119 and RFC 8174. @@ -119,7 +119,7 @@ No other operator's exponents are constrained. A tensor record encodes "no expon This step catches what load-time planning cannot: a divisibility that holds at the declared sizes but not at a smaller one; a kernel larger than a small padded input; inputs of `add`, `concat`, `crop_to` or `subpos_merge` whose sizes disagree; a `crop` whose scalars do not fit; a state whose shape no longer matches (§10.3); 4. evaluation. It starts with the inputs, in input-table order, state inputs included: each input's policy applies to it, so a `clamp` input is clamped, and an element of a `check` input outside its range refuses the decode (`field_range`). Then the operators run one by one in graph order. When a `check` tensor has been produced, an element outside its range refuses the decode (`field_range`). -A capability whose payload extension uses one code for every refusal reports that code in place of these. Examples are `wai.neural.int_synth` and `wai.neural.int_mlicpp`, which report `payload_malformed` (integer-payloads §8). +A capability whose payload extension uses fewer codes reports its own in place of these. `wai.neural.int_synth` reports `payload_malformed` for every refusal (integer-payloads §8); `wai.neural.int_mlicpp` reports `payload_malformed` for a refusal of its payload or of a value its decode forms, and `model_mismatch` for a bundle that does not fit (integer-payloads §14). A capability's driver MAY plan its graphs before it decodes any stream, at the point where its payload extension plans its networks. It then reports its extension's codes, in its extension's order, and step 3 never refuses in a decode that reaches it. @@ -1542,7 +1542,7 @@ No committed output changes, and no committed refusal fixture changes its code. - A shift from 64 to 127 now has its exact meaning. - The caps apply. 2. **`wai.world.model`'s integer renderer** refuses values outside its lowered `check` ranges, and a set whose `afrac` is outside `1..=30` or differs between its two networks. -3. **`WHM1` and `WAM1` layers.** A layer whose predicate fails at 0 (a bias of magnitude `2^63 − 1` or more) is refused at load, not at decode. So is a set in which a `check` range cannot hold any value its producer can give (§8.1), which today refuses every decode with `field_range`. Each is refused with `model_mismatch`, before any stream is decoded, where the direct decoder refused with `field_range`, or with a stream's code when the stream failed first. A network with no layer of tag 0, 1 or 2, and one whose header `shift` or `gf` is 128 or more where no layer uses it, are refused too (§13.2); the direct decoder decoded each where the shapes allowed. These take effect for `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper` and `wai.video.int_hyper` in this revision; for `wai.audio.int_codec` the `check` ranges include the PCM step's (§13.3), so a set whose last layer cannot give a sample inside it is refused at load, where every decode was refused at that step. For the conditional codec either network can carry the change, and the set is refused after its plan and its shape checks and before either stream (integer-payloads §7), where the direct decoder refused with the code of whichever stream or network failed first. +3. **`WHM1` and `WAM1` layers.** A layer whose predicate fails at 0 (a bias of magnitude `2^63 − 1` or more) is refused at load, not at decode. So is a set in which a `check` range cannot hold any value its producer can give (§8.1), which today refuses every decode with `field_range`. Each is refused with `model_mismatch`, before any stream is decoded, where the direct decoder refused with `field_range`, or with a stream's code when the stream failed first. A network with no layer of tag 0, 1 or 2, and one whose header `shift` or `gf` is 128 or more where no layer uses it, are refused too (§13.2); the direct decoder decoded each where the shapes allowed. These take effect for `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and `wai.neural.int_mlicpp` in this revision; for `wai.audio.int_codec` the `check` ranges include the PCM step's (§13.3), so a set whose last layer cannot give a sample inside it is refused at load, where every decode was refused at that step. For the conditional codec either network can carry the change, and the set is refused after its plan and its shape checks and before either stream (integer-payloads §7), where the direct decoder refused with the code of whichever stream or network failed first. For `wai.neural.int_mlicpp` any of its three networks can, and the bundle is refused after the header and the networks' geometry are checked and before either stream (integer-payloads §14). Its direct decoder made four more of these checks only when a network ran, after the streams and networks before it: a layer that does not fit the channels the layer before it gives; a network whose input is not the payload's channel count; a header `shift` or `gf` of 128 or more that a layer uses; and a leaky ReLU's shift of 64 or more. It refused each with `model_mismatch` too, so only the order changes: where a stream or a value was refused first, the bundle now is. Its graphs' run-time plan also caps a convolution's padded side at `2^26` (§4, §5.2), which its direct decoder did not; such a bundle is refused (`model_mismatch`). 4. **`wai.image.jpegai`.** - Its exact decoder refuses a malformed latent or hyper-latent stream (`stream_exhausted`) where it panics today. - It refuses a bundle that fails §13.6.1's load checks (`model_mismatch`), including a bound above 255 or a shift outside `0..=127`, which today's loader wraps. diff --git a/wai/extensions/integer-payloads.md b/wai/extensions/integer-payloads.md index 42df7136f..daf5cbf6c 100644 --- a/wai/extensions/integer-payloads.md +++ b/wai/extensions/integer-payloads.md @@ -721,5 +721,23 @@ The decode: bytes. The streams decode with `t2`, `lenient` and `final_only` -([integer-operators](integer-operators.md) E1). Every refusal of the payload, the -bundle or the decode is `payload_malformed` (§8). +([integer-operators](integer-operators.md) E1). A refusal of the payload, of either +stream or of a value the decode forms is `payload_malformed`; a bundle that breaks its +rules, or that does not fit the payload (step 1, and the load checks below), is +`model_mismatch` (§8). + +The reference decoder (`int_mlicpp::decode`) runs the three networks through their +lowering, `wmb1/1` ([integer-operators](integer-operators.md) §13.4: each network by +`whm1/1`, `g_s` with the RGB8 step), each graph loaded under `acc64`. Step 1 checks the +header and plans the networks from their layer geometry, then lowers and loads them, +which makes the set's checks of integer-operators §13.2 (a layer of tag 0, 1 or 2 in +each network; each layer fits the channels the layer before it gives; header shifts +under 128; a leaky shift under 64; no bias of magnitude `2^63 − 1` or more; the +certificate), then plans each graph on the payload's shapes (integer-operators §4: its +input's channel count, `Cz`, `3·Cy` or `Cy`, and the caps, a convolution's padded side +included), all before either stream. A value outside a graph's `check` range is refused +(`payload_malformed`): the ranges refuse exactly the values a layer's arithmetic could +leave `i64` on. Some of these checks are new refusals of input no committed corpus +contains, and some now come before the streams where an earlier decoder made them when +the network ran ([integer-operators](integer-operators.md) §15.3, SPEC §8 *Planned +breaks*). diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index cccef49b7..2467e8be3 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -814,7 +814,7 @@ required-features = ["video_int", "audio_int", "splat_int"] [[test]] name = "int_payload_cost" path = "tests/int_payload_cost.rs" -required-features = ["video_int", "audio_int", "splat_int"] +required-features = ["video_int", "audio_int", "splat_int", "legacy_int"] # The store of loaded parameter sets under the decodes that use it (src/int_set_cache.rs). [[test]] diff --git a/wai/wai-rs/src/int_mlicpp.rs b/wai/wai-rs/src/int_mlicpp.rs index a2579da2a..96f2d1c37 100644 --- a/wai/wai-rs/src/int_mlicpp.rs +++ b/wai/wai-rs/src/int_mlicpp.rs @@ -34,8 +34,14 @@ //! channel-major then raster: `b = det_build_index(σ, scale_edges)`, `r =` rANS //! decode against table `b`, `y = r·2^afrac + μ`. One `y` stream carries every //! slice's two passes in that order. -//! 4. `g_s(y)`, clamped to `[0, 2^afrac]` and rounded to 8 bits -//! ([`int_hyper_synth::to_rgb8`](crate::int_hyper_synth::to_rgb8)): RGB, HWC. +//! 4. `g_s(y)`, clamped to `[0, 2^afrac]` and rounded to 8 bits (the RGB8 step): RGB, +//! HWC. +//! +//! The three networks run through the integer operator set +//! (`extensions/integer-operators.md`): the bundle is lowered by `wmb1/1` (§13.4), each +//! network by `whm1/1` (§13.2, `g_s` with the RGB8 step of §13.0), and each graph is +//! loaded under `acc64` with its bounds certificate ([`ops`]). The stage steps (the two +//! streams, the buckets, the coding order) are this module's. //! //! This mirrors the float decode orchestration of `neural/mlicpp.rs` step for step; //! the networks are the sink-supplied model. The reference encoder, which also writes @@ -70,20 +76,21 @@ //! ([`net_shape`](crate::int_hyper_synth::net_shape)), and the decode is refused — //! the model does not fit this payload — unless they are the ones the header declares //! and no buffer the decode forms (a tensor the header sizes, or any activation on the -//! way) exceeds [`Header::MAX_TENSOR`] elements. Every latent goes through [`dequantize`], which refuses a value `i64` -//! cannot hold, and every network through -//! [`run_net_checked`](crate::int_hyper_synth::run_net_checked), which refuses -//! activations a layer's arithmetic could overflow on. The model is checked when it -//! is loaded: its networks are parsed without trusting the counts they carry, and its -//! tables validated (well-formed CDFs, consistent lengths), which is the -//! precondition under which the rANS decoder's arithmetic is proved total -//! (`rans.rs`). So `decode` returns an image or an error for any payload, and a build -//! with overflow checks and one without return the same one. Pure integer, no I/O. +//! way) exceeds [`Header::MAX_TENSOR`] elements; then each network is lowered and loaded, +//! and planned on the payload's shapes, before either stream is read ([`ops`]). Every +//! latent goes through [`dequantize`], which refuses a value `i64` cannot hold, and every +//! network through its lowered graph, whose `check` ranges refuse activations a layer's +//! arithmetic could overflow on (integer-operators §13.2). The model is checked when it is +//! loaded: its networks are parsed without trusting the counts they carry, and its tables +//! validated (well-formed CDFs, consistent lengths), which is the precondition under which +//! the rANS decoder's arithmetic is proved total (`rans.rs`). So `decode` returns an image +//! or an error for any payload, and a build with overflow checks and one without return +//! the same one. Pure integer, no I/O. use serde_json::Value; use crate::int_entropy::{dequantize, det_build_index}; -use crate::int_hyper_synth::{net_shape, run_net_checked, to_rgb8, CondEntropy, IntModel}; +use crate::int_hyper_synth::{net_shape, CondEntropy, IntModel}; use crate::rans::RansDecoder; /// Payload magic. @@ -377,9 +384,12 @@ pub fn decode_bytes(payload: &[u8], bundle: &[u8]) -> Result Result { - legacy::decode(model, payload) + ops::decode(model, payload) } /// [`decode`], also returning the `y` stream's [`Trace`] — as far as the decode got, @@ -388,7 +398,7 @@ pub fn decode(model: &IntMlicModel, payload: &[u8]) -> Result (Trace, Result) { - legacy::decode_traced(model, payload) + ops::decode_traced(model, payload) } /// Check, from `net`'s layer geometry alone, that on a `[c, h, w]` input it gives @@ -506,9 +516,13 @@ fn latent( /// integer operator set: `h_s`, `entropy` and `g_s` run layer by layer by /// [`run_net_checked`], which checks each layer's arithmetic on the values it is given /// before it runs, then the RGB8 bytes formed here ([`to_rgb8`]). The stage steps -/// ([`check`], [`hyper_latent`], [`latent`]) are the decode's own. +/// ([`check`], [`hyper_latent`], [`latent`]) are the decode's own. Test-only (feature +/// `legacy_int`): the differential tests decode both ways, and the two differ only on the +/// planned breaks of `extensions/integer-operators.md` §15.3. +#[cfg(any(test, feature = "legacy_int"))] pub mod legacy { use super::*; + use crate::int_hyper_synth::{run_net_checked, to_rgb8}; /// Run `net` on `x` (`[c, h, w]`) through [`run_net_checked`], after [`preflight`]: /// the payload is refused if its values could take a layer out of the exact integer diff --git a/wai/wai-rs/src/int_ops.rs b/wai/wai-rs/src/int_ops.rs index 94f346621..9d1d44403 100644 --- a/wai/wai-rs/src/int_ops.rs +++ b/wai/wai-rs/src/int_ops.rs @@ -42,6 +42,7 @@ pub(crate) const PROFILE: Profile = Profile::Acc64; /// ([`crate::int_set_cache::SetKey`]). pub const WHM1: &str = "whm1/1"; pub const WAM1: &str = "wam1/1"; +pub const WMB1: &str = "wmb1/1"; pub const INT_SYNTH: &str = "int_synth/1"; /// Load a lowered graph as a sink does: from its canonical bytes, under [`PROFILE`]. diff --git a/wai/wai-rs/src/int_payload/mod.rs b/wai/wai-rs/src/int_payload/mod.rs index fc1c366e1..a3504e806 100644 --- a/wai/wai-rs/src/int_payload/mod.rs +++ b/wai/wai-rs/src/int_payload/mod.rs @@ -412,13 +412,19 @@ pub fn decode_int(cap: &str, payload: &[u8], params: &IntParams<'_>) -> Result { - use crate::int_mlicpp::IntMlicError; + use crate::int_mlicpp::{ops, IntMlicError, IntMlicModel}; let mlic = |e: IntMlicError| match e { IntMlicError::Payload(why) => IntPayloadError::new(Refusal::PayloadMalformed, why), IntMlicError::Model(why) => model(why), }; - let m = crate::int_mlicpp::IntMlicModel::from_bundle(params.member(cap, "model.wmb")?).map_err(mlic)?; - let img = crate::int_mlicpp::decode(&m, payload).map_err(mlic)?; + // `int_mlicpp::decode`'s steps, its networks from the store when it holds + // the bundle: the header and the networks' geometry, the three networks + // lowered and loaded, each planned on the payload's shapes, then the decode. + let m = IntMlicModel::from_bundle(params.member(cap, "model.wmb")?).map_err(mlic)?; + let hd = ops::check(&m, payload).map_err(mlic)?; + let n = loaded(cap, crate::int_ops::WMB1, params, || ops::load(&m)).map_err(mlic)?; + ops::fit(&n, &hd).map_err(mlic)?; + let img = ops::decode_loaded(&n, &m.tables, m.afrac(), &hd, payload, None).map_err(mlic)?; Ok(IntDecoded::Image { rgb: img.rgb, width: img.width as u32, height: img.height as u32 }) } _ => Err(unsupported(cap, "no integer decoder for this capability")), @@ -564,7 +570,7 @@ fn loaded( use crate::int_set_cache::{LoadedSets, SetKey}; // The key names the capability by the constant this crate registers it under. let key = || { - let cap = [CAP_NEURAL_INT_HYPER, CAP_VIDEO_INT_HYPER, CAP_AUDIO_INT_CODEC, CAP_SPLAT_INT_CODEC, CAP_NEURAL_INT_SYNTH] + let cap = [CAP_NEURAL_INT_HYPER, CAP_VIDEO_INT_HYPER, CAP_AUDIO_INT_CODEC, CAP_SPLAT_INT_CODEC, CAP_NEURAL_INT_SYNTH, CAP_NEURAL_INT_MLICPP] .into_iter().find(|c| *c == cap)?; Some(SetKey { capability: cap, lowering, profile: crate::int_ops::PROFILE, digest: params.read_digest(cap)? }) }; diff --git a/wai/wai-rs/src/int_set_cache.rs b/wai/wai-rs/src/int_set_cache.rs index b77c965d9..3e766b759 100644 --- a/wai/wai-rs/src/int_set_cache.rs +++ b/wai/wai-rs/src/int_set_cache.rs @@ -67,6 +67,14 @@ impl Resident for crate::int_hyper_synth::ops::Nets { } } +/// The MLIC++-topology codec's three networks (`int_mlicpp::ops::Nets`), here for the same +/// reason. +impl Resident for crate::int_mlicpp::ops::Nets { + fn heap_bytes(&self) -> u64 { + self.h_s.graph().heap_bytes() + self.entropy.graph().heap_bytes() + self.g_s.graph().heap_bytes() + } +} + /// The key of a loaded set: the capability whose driver loaded it, the lowering /// (integer-operators §13), the profile its graphs were loaded under (§11), and the /// SHA-256 of the members it was lowered from, as [`SetKey::of`] forms it. diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs index 1041c7771..1e55022a3 100644 --- a/wai/wai-rs/tests/int_mlicpp_ops.rs +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -509,3 +509,93 @@ fn every_kernel_policy_decodes_through_the_lowering_alike() { } assert!(refused >= 3, "the cut stream and the two medians one past the edge are refused: {refused}"); } + +/// Every committed `WIM1` payload, alone or inside another file (an envelope, an HTTP +/// conformance object): each occurrence of the magic in the corpora that hold integer +/// payloads whose bytes from there parse as a `WIM1` of exactly the length its header's +/// stream lengths give. Returns `(where, payload)` pairs. +fn committed_wim1_payloads() -> Vec<(String, Vec)> { + fn files(d: &std::path::Path, out: &mut Vec) { + let Ok(rd) = std::fs::read_dir(d) else { return }; + let mut es: Vec<_> = rd.filter_map(|e| e.ok()).map(|e| e.path()).collect(); + es.sort(); + for p in es { + if p.is_dir() { files(&p, out) } else { out.push(p) } + } + } + let mut all = Vec::new(); + for d in ["int-payload-conformance", "staged-conformance", "staged-measure-conformance", "http-conformance", "pin-conformance", "signer-conformance", "wai-rs/tests/vectors"] { + files(&wai().join(d), &mut all); + } + let u32_at = |b: &[u8], i: usize| b.get(i..i + 4).map(|s| u32::from_le_bytes(s.try_into().unwrap()) as usize); + let mut v = Vec::new(); + for f in all { + let b = std::fs::read(&f).unwrap(); + for i in (0..b.len().saturating_sub(3)).filter(|&i| &b[i..i + 4] == b"WIM1") { + let (Some(lz), Some(ly)) = (u32_at(&b, i + 28), u32_at(&b, i + 32)) else { continue }; + let Some(end) = (i + 36).checked_add(lz).and_then(|e| e.checked_add(ly)) else { continue }; + if let Some(p) = b.get(i..end).filter(|p| mlic::parse_header(p).is_ok()) { + v.push((format!("{}@{i}", f.strip_prefix(wai()).unwrap().display()), p.to_vec())); + } + } + } + v +} + +/// The scan for the load rules the switch brings (integer-operators §15.3 item 3, and the +/// rules `int_mlicpp`'s networks now meet before either stream): no committed bundle or +/// payload is affected. Every committed bundle (the two sets indexed with their +/// certificates, and the HTTP corpus's copy of the vector's) has, in each network, a layer +/// of tag 0, 1 or 2, header shifts under 128, no bias of magnitude `2^63 − 1` or more, +/// layers that fit the channels before them and no leaky shift of 64 or more, and its +/// three networks lower and load under `acc64`. Every committed `WIM1` payload, wherever it +/// is carried, is planned through the lowering as directly, against the vector's bundle, +/// then decodes through the lowering to the bytes the direct decode gives, with the same +/// buckets and symbols, or is refused by both with the same code. (The other file that +/// holds the magic is code: the browser crate's recorded build.) +#[test] +fn the_new_load_rules_affect_no_committed_set_or_payload() { + use wai_ops::lower::NetLayer as N; + let mut bundles: Vec<(String, Vec)> = indexed().into_iter().map(|s| (s.name.clone(), read(&s.bundle))).collect(); + let copy = format!("http-conformance/sets/{}", indexed()[0].digest); + bundles.push((copy.clone(), read(©))); + assert_eq!(sha256(&bundles[2].1), indexed()[0].digest, "the HTTP corpus's copy is the vector's bundle"); + for (name, b) in &bundles { + let secs = sections(b); + for (k, (role, rgb8)) in [("h_s", false), ("entropy", false), ("g_s", true)].into_iter().enumerate() { + let net = wai_ops::lower::parse_whm1(&secs[k]).unwrap(); + assert!(net.layers.iter().any(|l| matches!(l, N::ConvT { .. } | N::Conv { .. } | N::Igdn { .. })), "{name} {role}: no layer of tag 0, 1 or 2"); + assert!(net.shift < 128 && net.gf < 128, "{name} {role}: a header shift of 128 or more"); + let mut c: Option = None; + for l in &net.layers { + match l { + N::ConvT { inc, out, bias, .. } | N::Conv { inc, out, bias, .. } => { + assert!(bias.iter().all(|b| b.unsigned_abs() < i64::MAX as u64), "{name} {role}: a bias of magnitude 2^63 - 1 or more"); + assert!(c.is_none_or(|c| c == *inc), "{name} {role}: a layer that does not fit the channels before it"); + c = Some(*out); + } + N::Igdn { c: cc, .. } => assert!(c.is_none_or(|c| c == *cc), "{name} {role}: an inverse GDN that does not fit"), + N::Leaky { slope_frac, .. } => assert!(*slope_frac < 64, "{name} {role}: a leaky shift of 64 or more"), + N::Relu => {} + } + } + let g = wai_ops::lower::whm1(&secs[k], rgb8).unwrap_or_else(|r| panic!("{name} {role}: does not lower: {r:?}")); + assert!(wai::int_ops::load(&g).is_ok(), "{name} {role}: does not load under acc64"); + } + mlic::ops::load(&IntMlicModel::from_bundle(b).unwrap()).unwrap_or_else(|e| panic!("{name}: {e}")); + } + let m = model(); + let nets = mlic::ops::load(&m).unwrap(); + let (mut decoded, mut refused) = (0, 0); + let payloads = committed_wim1_payloads(); + for (at, p) in &payloads { + if let Ok(hd) = mlic::ops::check(&m, p) { + mlic::ops::fit(&nets, &hd).unwrap_or_else(|e| panic!("{at}: the bundle fits the payload directly, and not through the lowering: {e}")); + } + let (ops, direct) = both(&m, p); + assert!(ops == direct, "{at}: the decode through the lowering differs from the direct decode"); + if ops.1.is_ok() { decoded += 1 } else { refused += 1 } + } + let http = payloads.iter().filter(|(at, _)| at.starts_with("http-conformance")).count(); + assert!(decoded >= 4 && http >= 2, "the committed WIM1 payloads, at least those of this revision: {decoded} decoded, {refused} refused, {http} in the HTTP corpus"); +} diff --git a/wai/wai-rs/tests/int_payload_cost.rs b/wai/wai-rs/tests/int_payload_cost.rs index e87973637..329b4fa6a 100644 --- a/wai/wai-rs/tests/int_payload_cost.rs +++ b/wai/wai-rs/tests/int_payload_cost.rs @@ -23,9 +23,13 @@ //! the 2^30-byte cap. It is decoded in release builds only (CI's release step); a debug //! build decodes a smaller clip of the same kind. //! +//! `wai.neural.int_mlicpp` declares no cost, so its decode through the integer operator set +//! is held to the direct decode it replaced (feature `legacy_int`): with its bundle loaded, +//! it holds no more than the direct decode on the same payload. +//! //! Its own test binary with a single test, so that no other test's allocations land in //! the window it measures. -#![cfg(all(feature = "video_int", feature = "audio_int", feature = "splat_int"))] +#![cfg(all(feature = "video_int", feature = "audio_int", feature = "splat_int", feature = "legacy_int"))] use std::alloc::{GlobalAlloc, Layout, System}; use std::collections::BTreeMap; @@ -281,4 +285,48 @@ fn the_declared_cost_bounds_what_the_decode_holds() { r.unwrap(); assert!(peak >= cost.output_bytes && peak <= cost.working_bytes, "int_hyper, set loaded: held {peak}, declared {}", cost.working_bytes); println!("int_hyper, its set loaded beforehand: declared {} working bytes; held {peak}", cost.working_bytes); + + // wai.neural.int_mlicpp declares no cost: integer-payloads §7 declares one for the + // formats SPEC §5 pins, and `check_cost` passes this one. So what its decode holds is + // held to the direct decode it replaced (`int_mlicpp::legacy`, test-only): with the + // bundle parsed and its three networks loaded beforehand, the decode through the operator + // set holds at least the image and no more than the direct decode holds on the same + // payload, both on the reference kernels. Through the sink, with the store on, the + // decode that loads the bundle holds within what the direct decode and the set as parsed, + // lowered and loaded hold, and the store counts at least what it keeps; the decode that + // finds the bundle held holds less, and keeps nothing more behind. + { + use wai::int_mlicpp::{legacy, ops, IntMlicModel}; + let bundle = read("int_mlicpp/model.wmb"); + let p = wai::container::Wai::from_bytes(&read("int_mlicpp/stream.wai")).unwrap().payload; + let (m, parse) = peak_during(|| IntMlicModel::from_bundle(&bundle).unwrap()); + let (want, direct) = with_policy(Policy::reference(), || peak_during(|| legacy::decode(&m, &p))); + let want = want.unwrap(); + let hd = ops::check(&m, &p).unwrap(); + let (nets, set) = peak_during(|| ops::load(&m).unwrap()); + ops::fit(&nets, &hd).unwrap(); + let (got, held) = with_policy(Policy::reference(), || peak_during(|| ops::decode_loaded(&nets, &m.tables, m.afrac(), &hd, &p, None))); + assert!(got.unwrap() == want, "int_mlicpp: the image through the lowering is the direct decode's"); + let out = 3 * (hd.height * hd.width) as u64; + assert!(held >= out && held <= direct, "int_mlicpp, set loaded: held {held}; the direct decode held {direct}"); + println!("int_mlicpp, its set loaded beforehand: held {held}; the direct decode held {direct}; loading the networks held {set}"); + let params = unpinned(c::CAP_NEURAL_INT_MLICPP, HeldBytes::File(&bundle)); + let (r, off) = with_policy(Policy::reference(), || peak_during(|| decode_int(c::CAP_NEURAL_INT_MLICPP, &p, ¶ms))); + drop(r.unwrap()); + store.set_budget(LoadedSets::DEFAULT_BUDGET); + let counted0 = store.stats().resident_bytes; + let base = CUR.load(Relaxed); + let (r, miss) = with_policy(Policy::reference(), || peak_during(|| decode_int(c::CAP_NEURAL_INT_MLICPP, &p, ¶ms))); + drop(r.unwrap()); + let kept = (CUR.load(Relaxed) - base) as u64; + let counted = store.stats().resident_bytes - counted0; + assert!(kept <= counted && counted - kept <= LoadedSets::INDEX_BYTES_PER_ENTRY, "int_mlicpp: the store holds {kept} bytes for the set and counts {counted}"); + let (r, hit) = with_policy(Policy::reference(), || peak_during(|| decode_int(c::CAP_NEURAL_INT_MLICPP, &p, ¶ms))); + drop(r.unwrap()); + assert_eq!((CUR.load(Relaxed) - base) as u64, kept, "int_mlicpp: a decode that finds its set held keeps nothing more"); + assert!(miss <= parse + set + direct, "int_mlicpp, store on: the decode that loads the set held {miss}, over the bundle parsed ({parse}), loaded ({set}) and the direct decode's {direct}"); + assert!(hit < miss && hit <= parse + direct, "int_mlicpp, store on: held {hit} finding the set, over the bundle parsed ({parse}) and the direct decode's {direct}; loading it held {miss}"); + println!("int_mlicpp, through the sink: store off held {off}; store on, the decode that loads the set held {miss}, the one that finds it held {hit}; the store holds {kept} and counts {counted}"); + store.set_budget(0); + } } diff --git a/wai/wai-rs/tests/int_set_cache.rs b/wai/wai-rs/tests/int_set_cache.rs index 8bbc4a01a..40019715b 100644 --- a/wai/wai-rs/tests/int_set_cache.rs +++ b/wai/wai-rs/tests/int_set_cache.rs @@ -5,13 +5,14 @@ //! - A second decode through the same set is a hit, by the store's count, and gives the //! same bytes as a decode with the store off, for every committed vector that decodes //! against a set: `tiny`, `video_nk`, `audio` and `splat` of the int-payload corpus, -//! and the int_synth corpus. +//! the int_synth corpus, and the `wai.neural.int_mlicpp` vector. //! - Two sets whose members have the same names and whose bytes differ in one byte never //! share an entry. //! - A set the load refuses is refused again, with the same refusal, while a set one //! byte away from it is held, and it is never kept: for int_synth, a set the bounds -//! pass refuses (load step 8); for the conditional codec, a set whose lowering leaves -//! a layer no admissible input range (integer-operators §13.2). +//! pass refuses (load step 8); for the conditional codec and the MLIC++-topology codec, +//! a set whose lowering leaves a layer no admissible input range (integer-operators +//! §13.2). //! - Every refusal fixture of the int-payload corpus keeps its code and its detail with //! its set held, and a payload refused while it is parsed never reaches the store. //! - Under a small budget the least recently used set goes first, a set past the budget @@ -110,6 +111,7 @@ fn registered(cap: &str) -> (&'static str, &'static str) { c::CAP_AUDIO_INT_CODEC => (c::CAP_AUDIO_INT_CODEC, wai::int_ops::WAM1), c::CAP_SPLAT_INT_CODEC => (c::CAP_SPLAT_INT_CODEC, wai::int_ops::WHM1), c::CAP_NEURAL_INT_SYNTH => (c::CAP_NEURAL_INT_SYNTH, wai::int_ops::INT_SYNTH), + c::CAP_NEURAL_INT_MLICPP => (c::CAP_NEURAL_INT_MLICPP, wai::int_ops::WMB1), other => panic!("{other} decodes against no set"), } } @@ -179,9 +181,29 @@ fn vectors() -> Vec<(String, String, Vec, Set)> { let dir = format!("neural-conformance/vectors/{name}"); v.push((format!("int_synth {name}"), c::CAP_NEURAL_INT_SYNTH.to_string(), read(&format!("{dir}/latent.bin")), synth_set(&read(&format!("{dir}/int_synth.json"))))); } + v.push(("int_mlicpp".to_string(), c::CAP_NEURAL_INT_MLICPP.to_string(), mlicpp_payload(), mlicpp_set(read(MLICPP_BUNDLE)))); v } +/// The `wai.neural.int_mlicpp` vector: its bundle, and the payload of its envelope. +const MLICPP_BUNDLE: &str = "wai-rs/tests/vectors/int_mlicpp/model.wmb"; +fn mlicpp_payload() -> Vec { + wai::container::Wai::from_bytes(&read("wai-rs/tests/vectors/int_mlicpp/stream.wai")).unwrap().payload +} +fn mlicpp_set(bundle: Vec) -> Set { + BTreeMap::from([("model.wmb".to_string(), bundle)]) +} + +/// The byte offset of a `WMB1` bundle's section `k` (0 `h_s`, 1 `entropy`, 2 `g_s`, 3 the +/// tables), past its length. +fn wmb1_section(b: &[u8], k: usize) -> usize { + let mut o = 4; + for _ in 0..k { + o += 4 + u32::from_le_bytes(b[o..o + 4].try_into().unwrap()) as usize; + } + o + 4 +} + #[test] fn a_second_decode_through_a_set_is_a_hit_and_gives_the_same_bytes() { @@ -200,8 +222,8 @@ fn a_second_decode_through_a_set_is_a_hit_and_gives_the_same_bytes() { assert_eq!((s1.misses - s0.misses, s1.hits - s0.hits, s1.kept - s0.kept), (1, 0, 1), "{name}: the first decode loads its set and keeps it"); assert_eq!((s2.misses - s1.misses, s2.hits - s1.hits, s2.kept - s1.kept), (0, 1, 0), "{name}: the second is a hit"); assert!(LoadedSets::global().contains(&key(&cap, &set)), "{name}: kept under the SHA-256 of its members"); - // The declared cost reads the same entry (int_synth has none). - if cap != c::CAP_NEURAL_INT_SYNTH { + // The declared cost reads the same entry (int_synth and int_mlicpp have none). + if cap != c::CAP_NEURAL_INT_SYNTH && cap != c::CAP_NEURAL_INT_MLICPP { let v = view(&set); let params = resolve(&cap, None, &[HeldBytes::Set { listing: b"", files: &v }], None).unwrap(); declared_cost(&cap, &payload, ¶ms).unwrap(); @@ -210,9 +232,9 @@ fn a_second_decode_through_a_set_is_a_hit_and_gives_the_same_bytes() { println!("{name}: {} bytes alike with the store off, on (miss) and on (hit)", off.len()); n += 1; } - assert_eq!(n, 8); + assert_eq!(n, 9); // tiny and video_nk read the same three members under two capabilities: two entries. - assert_eq!(stats().entries, 8); + assert_eq!(stats().entries, 9); } #[test] @@ -254,6 +276,25 @@ fn sets_one_byte_apart_never_share_an_entry() { assert_eq!((stats().misses, stats().hits, stats().entries), (4, 3, 4)); let (kx, ky) = (key(c::CAP_NEURAL_INT_SYNTH, &synth_set(&base)), key(c::CAP_NEURAL_INT_SYNTH, &synth_set(&other))); assert!(kx != ky && LoadedSets::global().contains(&kx) && LoadedSets::global().contains(&ky)); + + // int_mlicpp: g_s's last bias, channel 0, its fourth byte flipped, inside the bundle. + let (ma, mp) = (mlicpp_set(read(MLICPP_BUNDLE)), mlicpp_payload()); + let mut mb = ma.clone(); + let w = mb.get_mut("model.wmb").unwrap(); + let at = wmb1_section(w, 2); + let end = wmb1_section(w, 3) - 4; + let &(_, _, bias) = whm1_rq(&w[at..end]).last().unwrap(); + w[at + bias + 3] ^= 0x01; + let mc = c::CAP_NEURAL_INT_MLICPP; + let (ma_off, mb_off) = (decode_off(mc, &mp, &ma).unwrap(), decode_off(mc, &mp, &mb).unwrap()); + assert_ne!(ma_off, mb_off, "the byte changes the image"); + let s0 = stats(); + assert_eq!(decode(mc, &mp, &ma).unwrap(), ma_off); + assert_eq!(decode(mc, &mp, &mb).unwrap(), mb_off, "the bundle one byte away is decoded with its own networks"); + assert_eq!(decode(mc, &mp, &mb).unwrap(), mb_off); + assert_eq!((stats().misses - s0.misses, stats().hits - s0.hits), (2, 1)); + let (ka, kb) = (key(mc, &ma), key(mc, &mb)); + assert!(ka != kb && LoadedSets::global().contains(&ka) && LoadedSets::global().contains(&kb)); } #[test] @@ -302,6 +343,29 @@ fn a_set_the_load_refuses_stays_refused_beside_a_held_neighbour() { let after = stats(); assert_eq!((after.misses - held.misses, after.hits - held.hits, after.kept - held.kept), (2, 0, 0), "loaded each time, never kept"); assert!(!LoadedSets::global().contains(&key(cap, &bad)) && LoadedSets::global().contains(&key(cap, &good))); + + // int_mlicpp: the vector's bundle with g_s's layer-0 first bias at 2^63 − 1, refused as + // it is lowered, after the header and the networks' geometry; the vector's own bundle + // is held. + let (good, payload) = (mlicpp_set(read(MLICPP_BUNDLE)), mlicpp_payload()); + let mut bad = good.clone(); + let w = bad.get_mut("model.wmb").unwrap(); + let at = wmb1_section(w, 2); + let end = wmb1_section(w, 3) - 4; + let (_, _, bias) = whm1_rq(&w[at..end])[0]; + w[at + bias..at + bias + 8].copy_from_slice(&i64::MAX.to_le_bytes()); + let cap = c::CAP_NEURAL_INT_MLICPP; + let mlic_refused = decode_off(cap, &payload, &bad).expect_err("the bundle with that bias is refused"); + assert_eq!(mlic_refused.0, "model_mismatch"); + assert!(mlic_refused.1.starts_with("g_s does not lower to an integer-operator graph that loads under acc64"), "{mlic_refused:?}"); + decode(cap, &payload, &good).expect("the vector decodes, and is kept"); + let held = stats(); + for _ in 0..2 { + assert_eq!(decode(cap, &payload, &bad), Err(mlic_refused.clone()), "refused again, with the same refusal"); + } + let after = stats(); + assert_eq!((after.misses - held.misses, after.hits - held.hits, after.kept - held.kept), (2, 0, 0), "loaded each time, never kept"); + assert!(!LoadedSets::global().contains(&key(cap, &bad)) && LoadedSets::global().contains(&key(cap, &good))); } #[test] -- 2.54.0 (Apple Git-157) From ce755f2fa90eaeb65289d000a96d7ca0b0b95fb6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:46:37 -0400 Subject: [PATCH 21/22] int_mlicpp gate 7 (WIP: committed before running): the direct guard goes from production int_mlicpp was run_net_checked's last production caller. run_net_checked, check_layer, igdn_check and the values' failure constructor move into the test-only int_hyper_synth::legacy (cfg(any(test, feature = "legacy_int"))), beside the direct decodes that call them; the direct decodes of splat, audio and int_mlicpp, the guard's own unit tests and the integration tests that hold the guard and the check ranges to each other import them from there. Production docs that named the guard now name net_shape or the lowering. run_net and decode (the world renderer's) stay for M6. The export tool's note now names the check ranges. CHANGELOG. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 6 + wai/tools/wai_int_mlicpp_export.py | 6 +- wai/wai-rs/src/int_audio.rs | 4 +- wai/wai-rs/src/int_hyper_synth.rs | 342 ++++++++++++----------- wai/wai-rs/src/int_mlicpp.rs | 3 +- wai/wai-rs/src/int_splat.rs | 2 +- wai/wai-rs/tests/int_hyper_ops.rs | 2 +- wai/wai-rs/tests/int_mlicpp_ops.rs | 2 +- wai/wai-rs/tests/int_ops_differential.rs | 4 +- wai/wai-rs/tests/int_splat_ops.rs | 2 +- 10 files changed, 191 insertions(+), 182 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index cedb2ae65..d4a243f7e 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -37,6 +37,12 @@ wire format may change and such changes are recorded here. it, a network that does not read the payload's channel count, a header `shift` or `gf` of 128 or more that a layer uses, and a leaky ReLU's shift of 64 or more. A convolution whose padded side exceeds `2^26` is refused. +- **The direct guard is test-only.** With this decode on its lowering, no production + decode calls `int_hyper_synth::run_net_checked`, its per-layer checks or its IGDN check: + the `check` ranges of every lowered network make them. They move into the test-only + `int_hyper_synth::legacy` (feature `legacy_int`), beside the direct decodes that call + them for the tests that hold the two to each other. `run_net` and `decode`, the + unchecked path the integer world renderer uses, stay. - **The refusal codes, written down.** [integer-payloads](extensions/integer-payloads.md) §14 said that every refusal of this capability is `payload_malformed`. The decoder reports `model_mismatch` for a bundle diff --git a/wai/tools/wai_int_mlicpp_export.py b/wai/tools/wai_int_mlicpp_export.py index 737de6cfe..add09cf04 100644 --- a/wai/tools/wai_int_mlicpp_export.py +++ b/wai/tools/wai_int_mlicpp_export.py @@ -187,9 +187,9 @@ def run_checked(model, x, c): """Run an integer model on x ([c, h, w] int64 at 2^AFRAC) layer by layer with the ops `run_int_net` uses, after checking before every layer that no intermediate can leave int64 -- numpy int64 wraps silently, so these checks are what make the - result exact. They are at least as strict as the Rust decoder's - `int_hyper_synth::run_net_checked` (which accumulates IGDN in i128), so a vector - that passes here decodes there without refusal.""" + result exact. They are at least as strict as the `check` ranges of the Rust + decoder's lowered networks (integer-operators §13.2, which accumulates IGDN in + i128), so a vector that passes here decodes there without refusal.""" for i, L in enumerate(model['layers']): t = L['type'] xm = max_abs(x) diff --git a/wai/wai-rs/src/int_audio.rs b/wai/wai-rs/src/int_audio.rs index c0d254683..f1d88fea8 100644 --- a/wai/wai-rs/src/int_audio.rs +++ b/wai/wai-rs/src/int_audio.rs @@ -405,7 +405,7 @@ pub mod legacy { use crate::int_transform::igdn; /// One layer's arithmetic on the values `x` holds, checked before it runs, as - /// [`crate::int_hyper_synth::run_net_checked`] checks a 2-D layer: every product and + /// [`crate::int_hyper_synth::legacy::run_net_checked`] checks a 2-D layer: every product and /// partial sum and the requantised value of a convolution fit `i64`; IGDN's terms, /// norm and root fit their types. fn layer_in_range(model: &AudioModel, layer: &ALayer, x: &[i64], c: usize) -> Result<(), Refused> { @@ -427,7 +427,7 @@ pub mod legacy { } Ok(()) } - ALayer::Igdn { gamma, beta, .. } => crate::int_hyper_synth::igdn_check(x, c, gamma, beta, model.gf, model.afrac) + ALayer::Igdn { gamma, beta, .. } => crate::int_hyper_synth::legacy::igdn_check(x, c, gamma, beta, model.gf, model.afrac) .map_err(|(why, values)| refused(if values { RefusalKind::FieldRange } else { RefusalKind::ModelMismatch }, why)), } } diff --git a/wai/wai-rs/src/int_hyper_synth.rs b/wai/wai-rs/src/int_hyper_synth.rs index 2bbb0da61..edb5d95db 100644 --- a/wai/wai-rs/src/int_hyper_synth.rs +++ b/wai/wai-rs/src/int_hyper_synth.rs @@ -396,9 +396,9 @@ pub fn run_net(model: &IntModel, input: &[i64], mut c: usize, mut h: usize, mut (x, c, h, w) } -/// One layer of [`run_net`] on `x` (`[c, h, w]`): the new activations and shape. -/// [`run_net_checked`] runs layers through this same function, so the two cannot -/// compute different integers. +/// One layer of [`run_net`] on `x` (`[c, h, w]`): the new activations and shape. The +/// test-only direct guard (`legacy::run_net_checked`) runs layers through this same +/// function, so the two cannot compute different integers. fn apply_layer(model: &IntModel, layer: &IntLayer, mut x: Vec, c: usize, h: usize, w: usize) -> (Vec, usize, usize, usize) { let (afrac, gf, shift) = (model.afrac, model.gf, model.shift); match layer { @@ -430,11 +430,11 @@ fn apply_layer(model: &IntModel, layer: &IntLayer, mut x: Vec, c: usize, h: } } -/// Why [`run_net_checked`] declined to run a network: the index of the first layer -/// it would not run, the check that failed, and whether that check was about the -/// values presented (`out_of_range`: the layer is well formed, but these magnitudes -/// could overflow its arithmetic) or about the layer itself (malformed, or not -/// matching the input's shape). +/// Why a network was refused before it ran ([`net_shape`]), or by the test-only direct +/// guard (`legacy::run_net_checked`): the index of the first layer refused, the check +/// that failed, and whether that check was about the values presented (`out_of_range`: +/// the layer is well formed, but these magnitudes could overflow its arithmetic) or about +/// the layer itself (malformed, or not matching the input's shape). #[derive(Debug, Clone, PartialEq, Eq)] pub struct NetRefusal { pub layer: usize, @@ -448,82 +448,20 @@ impl std::fmt::Display for NetRefusal { } } -/// [`run_net`], run only while every step stays exact. -/// -/// Before each layer it checks, on the activations actually present, that: -/// -/// - the geometry is well formed, and every size the layer computes fits `usize` -/// (a transposed convolution's uncropped plane included); -/// - the weight, requantisation and IGDN buffers have exactly the lengths the layer -/// reads for the running channel count; -/// - every shift is below its operand's width; -/// - no product, partial sum or requantised value can leave the type the reference -/// kernel forms it in. For a convolution with `terms = c·k·k` inputs per output, -/// `terms · max|w| · max|x|` bounds every product and partial sum (each output -/// element adds at most `terms` products, whatever their order) and must fit `i64`; -/// the requantised value is at most `(bound · max|mult|) >> shift` plus one (the -/// floor of a negative) plus `max|bias|`, and must fit `i64` too. For IGDN, with -/// `X = max|x|`, `G = max|gamma|`, `B = max|beta|`: `G·X²` and -/// `B + c·((G·X²) >> gf + 1)` must fit `i128`, its integer square root (which the -/// reference narrows to `i64`) must fit `i64`, and the output `(X·root) >> afrac` -/// plus one must fit `i64`. For the leaky ReLU, `max|x| · |slope|` must fit `i64`. -/// -/// A layer that fails a check is not run, and the refusal says which and why. On -/// `Ok` the output is [`run_net`]'s, computed by the same code ([`apply_layer`], which -/// dispatches to the fast kernels or the reference as `run_net` does), and no step -/// overflowed, so a build with overflow checks and one without compute the same -/// integers and neither panics. The fast kernels, which narrow only under their own -/// bounds, form no value the reference does not (see [`crate::int_kernels`]). -/// -/// The checks are one layer deep, on real values, so they refuse only activations -/// that come within a factor `terms` (or, for IGDN, `c`) of the type's range, not -/// a network whose worst case over all inputs would. A decoder that feeds values it -/// read from an untrusted stream into a network uses this, and refuses the stream on -/// `Err`. -/// -/// A network that decodes through its lowering in the integer operator set -/// (`extensions/integer-operators.md` §13.2) makes these checks as the `check` ranges of -/// its graph instead, which refuse exactly what these refuse, at the same layer. The -/// splat, audio and conditional decodes do (`int_splat`, `int_audio`, -/// [`decode_conditional_checked`]; their direct decodes, which call this or -/// [`igdn_check`], are test-only). Its one production caller left is -/// `int_mlicpp`, whose networks move to their lowering (`wmb1/1`) next, and it goes -/// when they have. -pub fn run_net_checked( - model: &IntModel, input: &[i64], mut c: usize, mut h: usize, mut w: usize, -) -> Result<(Vec, usize, usize, usize), NetRefusal> { - let n = c.checked_mul(h).and_then(|v| v.checked_mul(w)); - if c == 0 || h == 0 || w == 0 || n != Some(input.len()) { - return Err(NetRefusal { layer: 0, reason: "input length is not channels × height × width", out_of_range: false }); - } - let mut x = input.to_vec(); - for (i, layer) in model.layers.iter().enumerate() { - let out = check_layer(model, layer, &x, c, h, w) - .map_err(|(reason, out_of_range)| NetRefusal { layer: i, reason, out_of_range })?; - let (nx, nc, nh, nw) = apply_layer(model, layer, x, c, h, w); - debug_assert_eq!((nc, nh, nw), out, "check_layer and the layer disagree on the output shape"); - (x, c, h, w) = (nx, nc, nh, nw); - } - Ok((x, c, h, w)) -} - -/// A check [`check_layer`] failed: its reason, and whether it concerned the values -/// presented (`true`) or the layer itself (`false`). +/// A check that failed: its reason, and whether it concerned the values presented +/// (`true`) or the layer itself (`false`). pub(crate) type Failed = (&'static str, bool); const fn shape(reason: &'static str) -> Failed { (reason, false) } -const fn range(reason: &'static str) -> Failed { - (reason, true) -} /// One layer's geometry on a `[c, h, w]` input, from its parameters alone: the output /// shape, and the most elements the reference kernel holds in one buffer while /// running it — for a transposed convolution the uncropped plane it accumulates into /// before cropping, otherwise the output. Every size is formed with checked /// arithmetic; a geometry that is malformed or overflows `usize` is refused. Shared -/// by [`check_layer`] and [`net_shape`], so the shape a caller plans for is the shape -/// the layer runs at. +/// by [`net_shape`] and the test-only direct guard, so the shape a caller plans for is +/// the shape the layer runs at. fn layer_geometry(layer: &IntLayer, c: usize, h: usize, w: usize) -> Result<((usize, usize, usize), usize), Failed> { let plane = |c: usize, h: usize, w: usize| c.checked_mul(h).and_then(|v| v.checked_mul(w)); match layer { @@ -579,10 +517,10 @@ pub struct NetShape { } /// [`NetShape`] for `model` on a `[c, h, w]` input, walking the layers' geometry -/// without allocating an activation or reading a value. Refuses what -/// [`run_net_checked`] would refuse on geometry — an empty input, a malformed layer, a -/// size that overflows `usize` — naming the same layer and reason; the parameter-length -/// and value checks are `run_net_checked`'s, made before each layer runs. +/// without allocating an activation or reading a value. Refuses an empty input, a +/// malformed layer, or a size that overflows `usize`, naming the layer and the reason; +/// the parameter lengths and the values are a network's lowering's to check +/// (integer-operators §13.2). /// /// A decoder that sizes a network's input from an untrusted header calls this first, /// and refuses before running anything a header whose shapes the network does not @@ -602,91 +540,6 @@ pub fn net_shape(model: &IntModel, mut c: usize, mut h: usize, mut w: usize) -> Ok(NetShape { out: (c, h, w), peak }) } -/// The checks [`run_net_checked`] documents, for one layer on activations `x` -/// (`[c, h, w]`). Returns the layer's output shape. -fn check_layer(model: &IntModel, layer: &IntLayer, x: &[i64], c: usize, h: usize, w: usize) -> Result<(usize, usize, usize), Failed> { - use crate::int_kernels::max_abs; - let (out, _) = layer_geometry(layer, c, h, w)?; - const I64: u128 = i64::MAX as u128; - let xm = max_abs(x) as u128; - // One (transposed) convolution's arithmetic: every product and partial sum, then - // the requantised value, must fit i64 (the reference's accumulator and result). - let conv_range = |cout: usize, k: usize, weight: &[i64], mult: &[i64], bias: &[i64]| -> Result<(), Failed> { - let w_len = c.checked_mul(cout).and_then(|v| v.checked_mul(k)).and_then(|v| v.checked_mul(k)); - if w_len != Some(weight.len()) || mult.len() != cout || bias.len() != cout { - return Err(shape("parameter lengths do not match the running channel count")); - } - if model.shift > 127 { - return Err(shape("requantisation shift of 128 or more")); - } - // c·k·k ≤ weight.len() (checked above), but saturate regardless. - let terms = (c as u128).saturating_mul(k as u128).saturating_mul(k as u128); - let acc = terms.saturating_mul(max_abs(weight) as u128).saturating_mul(xm); - if acc > I64 { - return Err(range("accumulation could leave i64")); - } - // acc ≤ 2^63 and |mult| ≤ 2^63, so the product fits u128. - let q = ((acc * max_abs(mult) as u128) >> model.shift) + 1; - if q.saturating_add(max_abs(bias) as u128) > I64 { - return Err(range("requantised value could leave i64")); - } - Ok(()) - }; - match layer { - IntLayer::ConvT { out_ch, k, weight, mult, bias, .. } | IntLayer::Conv { out_ch, k, weight, mult, bias, .. } => { - conv_range(*out_ch, *k, weight, mult, bias)?; - Ok(out) - } - IntLayer::Igdn { gamma, beta } => { - igdn_check(x, c, gamma, beta, model.gf, model.afrac)?; - Ok((c, h, w)) - } - IntLayer::Relu => Ok((c, h, w)), - IntLayer::Leaky { slope_q, slope_frac } => { - if *slope_frac > 63 { - return Err(shape("leaky slope shift of 64 or more")); - } - if xm.saturating_mul(slope_q.unsigned_abs() as u128) > I64 { - return Err(range("leaky product could leave i64")); - } - Ok((c, h, w)) - } - } -} - -/// The IGDN checks [`run_net_checked`] documents, for activations `x` of `c` channels -/// at `2^afrac` against `gamma` (`c × c`, at `2^gf`) and `beta` (`c`). Shared with the -/// test-only direct decode of `int_audio`, whose IGDN is the same [`igdn`]. -pub(crate) fn igdn_check(x: &[i64], c: usize, gamma: &[i64], beta: &[i64], gf: u32, afrac: u32) -> Result<(), Failed> { - use crate::int_kernels::max_abs; - const I64: u128 = i64::MAX as u128; - const I128: u128 = i128::MAX as u128; - if c.checked_mul(c) != Some(gamma.len()) || beta.len() != c { - return Err(shape("IGDN parameter lengths do not match the channel count")); - } - if gf > 127 || afrac > 127 { - return Err(shape("IGDN shift of 128 or more")); - } - let xm = max_abs(x) as u128; - // x² ≤ 2^126 always fits; each term gamma·x², and the norm, must fit i128. - let x2 = xm * xm; - let term = (max_abs(gamma) as u128).checked_mul(x2).filter(|&t| t <= I128).ok_or(range("IGDN term could leave i128"))?; - let norm = (max_abs(beta) as u128).saturating_add((c as u128).saturating_mul((term >> gf) + 1)); - if norm > I128 { - return Err(range("IGDN norm could leave i128")); - } - // The reference narrows the root to i64 (`isqrt(norm) as i64`), so it must - // fit; then |x · root| < 2^63 · 2^63 fits u128, and the result must fit i64. - let root = norm.isqrt(); - if root > I64 { - return Err(range("IGDN root could leave i64")); - } - if ((xm * root) >> afrac) + 1 > I64 { - return Err(range("IGDN output could leave i64")); - } - Ok(()) -} - /// Decode the integer synthesis `model` on the latent `input` (`[c, h, w]` at /// `2^afrac`) → RGB, row-major **HWC** `u8`. Byte-identical on every machine. pub fn decode(model: &IntModel, input: &[i64], c: usize, h: usize, w: usize) -> (Vec, usize, usize) { @@ -968,8 +821,8 @@ impl CondBitstream { /// /// Every size is formed in `u64` with checked arithmetic, a size past `u64` being over /// the cap, so a 32-bit sink refuses exactly what a 64-bit one does, with the same -/// kind; and once a network is planned, every size [`run_net_checked`] forms fits -/// `usize` on every target. The first rule that fails is the refusal. +/// kind; and once a network is planned, every size a decode forms for it fits `usize` on +/// every target. The first rule that fails is the refusal. pub(crate) fn plan_net(net: &IntModel, c: usize, h: usize, w: usize) -> Result { plan_layers(net, c, h, w) } @@ -1157,15 +1010,163 @@ fn latent(ent: &CondEntropy, bs: &CondStream<'_>, afrac: u32, sigma: &[i64], mu: } /// The direct decode, as the conditional codec ran before it decoded through the integer -/// operator set: `h_s` and `g_s` run layer by layer by [`run_net_checked`], which checks -/// each layer's arithmetic on the values it is given before it runs, then the RGB8 bytes -/// formed here ([`to_rgb8`]). Test-only (`legacy_int`): the differential tests decode both -/// ways, and the two differ only on the planned breaks of -/// `extensions/integer-operators.md` §15.3. +/// operator set: `h_s` and `g_s` run layer by layer by [`legacy::run_net_checked`], which +/// checks each layer's arithmetic on the values it is given before it runs, then the RGB8 +/// bytes formed here ([`to_rgb8`]). The guard itself is here too: the direct decodes of the +/// splat, audio and MLIC++-topology codecs call it (or its IGDN check), and no production +/// decode does. Test-only (`legacy_int`): the differential tests decode both ways, and the +/// two differ only on the planned breaks of `extensions/integer-operators.md` §15.3. #[cfg(any(test, feature = "legacy_int"))] pub mod legacy { use super::*; + /// [`run_net`], run only while every step stays exact. + /// + /// Before each layer it checks, on the activations actually present, that: + /// + /// - the geometry is well formed, and every size the layer computes fits `usize` + /// (a transposed convolution's uncropped plane included); + /// - the weight, requantisation and IGDN buffers have exactly the lengths the layer + /// reads for the running channel count; + /// - every shift is below its operand's width; + /// - no product, partial sum or requantised value can leave the type the reference + /// kernel forms it in. For a convolution with `terms = c·k·k` inputs per output, + /// `terms · max|w| · max|x|` bounds every product and partial sum (each output + /// element adds at most `terms` products, whatever their order) and must fit `i64`; + /// the requantised value is at most `(bound · max|mult|) >> shift` plus one (the + /// floor of a negative) plus `max|bias|`, and must fit `i64` too. For IGDN, with + /// `X = max|x|`, `G = max|gamma|`, `B = max|beta|`: `G·X²` and + /// `B + c·((G·X²) >> gf + 1)` must fit `i128`, its integer square root (which the + /// reference narrows to `i64`) must fit `i64`, and the output `(X·root) >> afrac` + /// plus one must fit `i64`. For the leaky ReLU, `max|x| · |slope|` must fit `i64`. + /// + /// A layer that fails a check is not run, and the refusal says which and why. On + /// `Ok` the output is [`run_net`]'s, computed by the same code ([`apply_layer`], which + /// dispatches to the fast kernels or the reference as `run_net` does), and no step + /// overflowed, so a build with overflow checks and one without compute the same + /// integers and neither panics. The fast kernels, which narrow only under their own + /// bounds, form no value the reference does not (see [`crate::int_kernels`]). + /// + /// The checks are one layer deep, on real values, so they refuse only activations + /// that come within a factor `terms` (or, for IGDN, `c`) of the type's range, not + /// a network whose worst case over all inputs would. A decoder that feeds values it + /// read from an untrusted stream into a network uses this, and refuses the stream on + /// `Err`. + /// + /// Every network that decodes in production does so through its lowering in the integer + /// operator set (`extensions/integer-operators.md` §13.2), which makes these checks as the + /// `check` ranges of its graph, refusing exactly what these refuse, at the same layer: the + /// splat, audio, conditional and MLIC++-topology decodes. This guard, and the direct + /// decodes that call it or [`igdn_check`], are test-only, for the tests that hold the two + /// to each other. + pub fn run_net_checked( + model: &IntModel, input: &[i64], mut c: usize, mut h: usize, mut w: usize, + ) -> Result<(Vec, usize, usize, usize), NetRefusal> { + let n = c.checked_mul(h).and_then(|v| v.checked_mul(w)); + if c == 0 || h == 0 || w == 0 || n != Some(input.len()) { + return Err(NetRefusal { layer: 0, reason: "input length is not channels × height × width", out_of_range: false }); + } + let mut x = input.to_vec(); + for (i, layer) in model.layers.iter().enumerate() { + let out = check_layer(model, layer, &x, c, h, w) + .map_err(|(reason, out_of_range)| NetRefusal { layer: i, reason, out_of_range })?; + let (nx, nc, nh, nw) = apply_layer(model, layer, x, c, h, w); + debug_assert_eq!((nc, nh, nw), out, "check_layer and the layer disagree on the output shape"); + (x, c, h, w) = (nx, nc, nh, nw); + } + Ok((x, c, h, w)) + } + + /// A check that concerned the values presented. + const fn range(reason: &'static str) -> Failed { + (reason, true) + } + + /// The checks [`run_net_checked`] documents, for one layer on activations `x` + /// (`[c, h, w]`). Returns the layer's output shape. + fn check_layer(model: &IntModel, layer: &IntLayer, x: &[i64], c: usize, h: usize, w: usize) -> Result<(usize, usize, usize), Failed> { + use crate::int_kernels::max_abs; + let (out, _) = layer_geometry(layer, c, h, w)?; + const I64: u128 = i64::MAX as u128; + let xm = max_abs(x) as u128; + // One (transposed) convolution's arithmetic: every product and partial sum, then + // the requantised value, must fit i64 (the reference's accumulator and result). + let conv_range = |cout: usize, k: usize, weight: &[i64], mult: &[i64], bias: &[i64]| -> Result<(), Failed> { + let w_len = c.checked_mul(cout).and_then(|v| v.checked_mul(k)).and_then(|v| v.checked_mul(k)); + if w_len != Some(weight.len()) || mult.len() != cout || bias.len() != cout { + return Err(shape("parameter lengths do not match the running channel count")); + } + if model.shift > 127 { + return Err(shape("requantisation shift of 128 or more")); + } + // c·k·k ≤ weight.len() (checked above), but saturate regardless. + let terms = (c as u128).saturating_mul(k as u128).saturating_mul(k as u128); + let acc = terms.saturating_mul(max_abs(weight) as u128).saturating_mul(xm); + if acc > I64 { + return Err(range("accumulation could leave i64")); + } + // acc ≤ 2^63 and |mult| ≤ 2^63, so the product fits u128. + let q = ((acc * max_abs(mult) as u128) >> model.shift) + 1; + if q.saturating_add(max_abs(bias) as u128) > I64 { + return Err(range("requantised value could leave i64")); + } + Ok(()) + }; + match layer { + IntLayer::ConvT { out_ch, k, weight, mult, bias, .. } | IntLayer::Conv { out_ch, k, weight, mult, bias, .. } => { + conv_range(*out_ch, *k, weight, mult, bias)?; + Ok(out) + } + IntLayer::Igdn { gamma, beta } => { + igdn_check(x, c, gamma, beta, model.gf, model.afrac)?; + Ok((c, h, w)) + } + IntLayer::Relu => Ok((c, h, w)), + IntLayer::Leaky { slope_q, slope_frac } => { + if *slope_frac > 63 { + return Err(shape("leaky slope shift of 64 or more")); + } + if xm.saturating_mul(slope_q.unsigned_abs() as u128) > I64 { + return Err(range("leaky product could leave i64")); + } + Ok((c, h, w)) + } + } + } + + /// The IGDN checks [`run_net_checked`] documents, for activations `x` of `c` channels + /// at `2^afrac` against `gamma` (`c × c`, at `2^gf`) and `beta` (`c`). Shared with the + /// test-only direct decode of `int_audio`, whose IGDN is the same [`igdn`]. + pub(crate) fn igdn_check(x: &[i64], c: usize, gamma: &[i64], beta: &[i64], gf: u32, afrac: u32) -> Result<(), Failed> { + use crate::int_kernels::max_abs; + const I64: u128 = i64::MAX as u128; + const I128: u128 = i128::MAX as u128; + if c.checked_mul(c) != Some(gamma.len()) || beta.len() != c { + return Err(shape("IGDN parameter lengths do not match the channel count")); + } + if gf > 127 || afrac > 127 { + return Err(shape("IGDN shift of 128 or more")); + } + let xm = max_abs(x) as u128; + // x² ≤ 2^126 always fits; each term gamma·x², and the norm, must fit i128. + let x2 = xm * xm; + let term = (max_abs(gamma) as u128).checked_mul(x2).filter(|&t| t <= I128).ok_or(range("IGDN term could leave i128"))?; + let norm = (max_abs(beta) as u128).saturating_add((c as u128).saturating_mul((term >> gf) + 1)); + if norm > I128 { + return Err(range("IGDN norm could leave i128")); + } + // The reference narrows the root to i64 (`isqrt(norm) as i64`), so it must + // fit; then |x · root| < 2^63 · 2^63 fits u128, and the result must fit i64. + let root = norm.isqrt(); + if root > I64 { + return Err(range("IGDN root could leave i64")); + } + if ((xm * root) >> afrac) + 1 > I64 { + return Err(range("IGDN output could leave i64")); + } + Ok(()) + } + /// [`run_net_checked`] with its refusal as a [`Refused`]: values that could overflow a /// layer are the stream's (`FieldRange`); a layer that does not fit is the model's. fn run_checked(net: &IntModel, x: &[i64], c: usize, h: usize, w: usize) -> Result, Refused> { @@ -1650,6 +1651,7 @@ mod kernel_equivalence { #[cfg(test)] mod checked { use super::*; + use super::legacy::{igdn_check, run_net_checked}; use crate::int_kernels::testkit::Rng; fn conv1(weight: Vec, mult: i64, bias: i64) -> IntLayer { diff --git a/wai/wai-rs/src/int_mlicpp.rs b/wai/wai-rs/src/int_mlicpp.rs index 96f2d1c37..5b533b5dc 100644 --- a/wai/wai-rs/src/int_mlicpp.rs +++ b/wai/wai-rs/src/int_mlicpp.rs @@ -522,7 +522,8 @@ fn latent( #[cfg(any(test, feature = "legacy_int"))] pub mod legacy { use super::*; - use crate::int_hyper_synth::{run_net_checked, to_rgb8}; + use crate::int_hyper_synth::legacy::run_net_checked; + use crate::int_hyper_synth::to_rgb8; /// Run `net` on `x` (`[c, h, w]`) through [`run_net_checked`], after [`preflight`]: /// the payload is refused if its values could take a layer out of the exact integer diff --git a/wai/wai-rs/src/int_splat.rs b/wai/wai-rs/src/int_splat.rs index 9ce3399b3..8460ff6d6 100644 --- a/wai/wai-rs/src/int_splat.rs +++ b/wai/wai-rs/src/int_splat.rs @@ -170,7 +170,7 @@ fn latent(st: &SplatStream<'_>, afrac: u32) -> Result, Refused> { #[cfg(any(test, feature = "legacy_int"))] pub mod legacy { use super::*; - use crate::int_hyper_synth::run_net_checked; + use crate::int_hyper_synth::legacy::run_net_checked; /// The direct decode of [`super::decode_typed`]'s arguments. pub fn decode_typed(gs: &IntModel, st: &SplatStream<'_>) -> Result { diff --git a/wai/wai-rs/tests/int_hyper_ops.rs b/wai/wai-rs/tests/int_hyper_ops.rs index 2a203eeec..36706d6ed 100644 --- a/wai/wai-rs/tests/int_hyper_ops.rs +++ b/wai/wai-rs/tests/int_hyper_ops.rs @@ -450,7 +450,7 @@ fn every_refusal_of_the_direct_decode_is_refused_alike_through_the_lowering() { /// what integer-payloads §7 *Exactness* refuses, at the same layer. #[test] fn every_check_range_refuses_exactly_what_the_direct_decode_refuses() { - use hyper::{run_net_checked, IntLayer}; + use hyper::{legacy::run_net_checked, IntLayer}; use wai_ops::{Code, Site, Tensor}; let mut seen = 0; for (net, rgb8) in [("tiny_h_s.bin", false), ("tiny_g_s.bin", true)] { diff --git a/wai/wai-rs/tests/int_mlicpp_ops.rs b/wai/wai-rs/tests/int_mlicpp_ops.rs index 1e55022a3..98c49630b 100644 --- a/wai/wai-rs/tests/int_mlicpp_ops.rs +++ b/wai/wai-rs/tests/int_mlicpp_ops.rs @@ -417,7 +417,7 @@ fn a_stream_that_runs_out_is_refused_at_its_end_alike() { /// the direct decode refuses, at the same layer. #[test] fn every_check_range_refuses_exactly_what_the_direct_decode_refuses() { - use wai::int_hyper_synth::{run_net_checked, IntLayer}; + use wai::int_hyper_synth::{legacy::run_net_checked, IntLayer}; use wai_ops::{Code, Site, Tensor}; let secs = sections(&read(&format!("{DIR}/model.wmb"))); let mut seen = 0; diff --git a/wai/wai-rs/tests/int_ops_differential.rs b/wai/wai-rs/tests/int_ops_differential.rs index 66e801178..48e39e4f2 100644 --- a/wai/wai-rs/tests/int_ops_differential.rs +++ b/wai/wai-rs/tests/int_ops_differential.rs @@ -1228,7 +1228,7 @@ fn int_hyper_case(r: &mut Rng) -> (Vec, Vec, Cond) { }) .collect(); let sigma: Option> = z_hat.and_then(|z| { - let (o, oc, oh, ow) = wai::int_hyper_synth::run_net_checked(&hm, &z, nz, zh, zw).ok()?; + let (o, oc, oh, ow) = wai::int_hyper_synth::legacy::run_net_checked(&hm, &z, nz, zh, zw).ok()?; ((oc, oh, ow) == (2 * mu, sh, sw)).then(|| o[..mu * sh * sw].to_vec()) }); let ny = mu * sh * sw; @@ -1536,7 +1536,7 @@ fn same_size_layer(r: &mut Rng, c: u32, out: u32) -> Whm { /// far, until a network refuses (the rest of the stream is then random); each stream /// damaged as [`damage`] says. fn int_mlicpp_case(r: &mut Rng) -> (Vec, Vec) { - use wai::int_hyper_synth::run_net_checked; + use wai::int_hyper_synth::legacy::run_net_checked; let afrac = match r.below(32) { 0 | 1 => 1 + r.below(30) as u32, 2 => [0, 31][r.below(2) as usize], diff --git a/wai/wai-rs/tests/int_splat_ops.rs b/wai/wai-rs/tests/int_splat_ops.rs index 0501fd1c3..cec7a1654 100644 --- a/wai/wai-rs/tests/int_splat_ops.rs +++ b/wai/wai-rs/tests/int_splat_ops.rs @@ -299,7 +299,7 @@ fn every_refusal_of_the_direct_decode_is_refused_alike_through_the_lowering() { /// *Exactness* refuses, at the same layer. #[test] fn every_check_range_refuses_exactly_what_the_direct_decode_refuses() { - use wai::int_hyper_synth::run_net_checked; + use wai::int_hyper_synth::legacy::run_net_checked; use wai_ops::{Code, Site, Tensor}; let set = read(SET); let full = wai_ops::lower::whm1(&set, true).unwrap(); -- 2.54.0 (Apple Git-157) From bad6baa59c4e7e91dbddec11121665aab31850c7 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 15 Oct 2026 22:46:44 -0400 Subject: [PATCH 22/22] int_mlicpp gate 7: the splat direct decode's doc names the guard's new path Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/int_splat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wai/wai-rs/src/int_splat.rs b/wai/wai-rs/src/int_splat.rs index 8460ff6d6..435ae7b17 100644 --- a/wai/wai-rs/src/int_splat.rs +++ b/wai/wai-rs/src/int_splat.rs @@ -163,7 +163,7 @@ fn latent(st: &SplatStream<'_>, afrac: u32) -> Result, Refused> { /// The direct decode, as the decoder ran before it decoded through the integer operator /// set: the synthesis `g_s` run layer by layer by -/// [`crate::int_hyper_synth::run_net_checked`], which checks each layer's arithmetic on +/// [`crate::int_hyper_synth::legacy::run_net_checked`], which checks each layer's arithmetic on /// the values it is given before it runs, then the attribute bytes formed here. /// Test-only (`legacy_int`): the differential tests decode both ways, and the two differ /// only on the planned breaks of `extensions/integer-operators.md` §15.3. -- 2.54.0 (Apple Git-157)