open-standards/wai/wai-rs/src/int_synth.rs
Transaction Science f1a359c90f wai-web: integer-exact neural decode in the browser, no ML runtime
int_synth + int_entropy are pure i64 (no ort, no float), so the SAME Rust that
decodes natively compiles to wasm32 and runs in the browser with no ML runtime
at all — and the bytes are identical to native on every machine. This is the
integer-entropy contract's payoff made visible: a learned-codec reconstruction
whose hash is portable.

- int_synth: factor the decode into `decode_from_spec(payload, spec_bytes)` (no
  std::fs); `decode(payload, path)` now reads the spec and delegates. WASM/no-FS
  sinks call the bytes entry point with host-supplied weights. Behavior-
  identical (4 int_synth tests green).
- wai-web: add the `neural_int` feature + a `neural_int` wasm-bindgen module:
  decodeIntSynth(payload, specJson) -> RGB, the int_entropy primitives
  (fixedScaleEdges / quantizeScale / detBuildIndex), and blake3Hex for the
  in-page receipt check. Compiles to wasm32 (web + nodejs targets).
- demo: int_synth.html/.js — decode a vector to a canvas and verify its BLAKE3
  against the frozen native hash in-page. verify_int_synth.sh +
  verify_int_synth_wasm.mjs — reproducible CLI proof.

Verified: all four conformance vectors (incl. the real trained 128×128 photo
model, 21.6 dB) decode in wasm32 byte-identical to the frozen NATIVE corpus
hashes. Deterministic learned-codec decode in-browser, zero JS ML runtime.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 05:53:41 -04:00

322 lines
13 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Integer fixed-point synthesis — a learned-image decoder whose reconstruction
//! is **bit-identical on every machine, by construction**.
//!
//! This is the deterministic member of the neural family. The integer rANS
//! recovers latents exactly; the divergence in float decoders like
//! `bmshj2018` / `mbt2018_mean` is the **float ONNX synthesis** — and float
//! round-off is platform-dependent. Here the synthesis is a stack of integer
//! transposed convolutions with integer activations: every operation is `i64`
//! arithmetic + an arithmetic shift, so two sinks honoring the spec produce the
//! same bytes everywhere. No ONNX, no float, nothing implementation-defined.
//!
//! Because it pulls in no float runtime, this decoder lives at the crate root
//! behind the light `neural_int` feature (just `serde_json`), not inside the
//! `ort`-backed `neural` module. That keeps it buildable — and its
//! cross-hardware bit-exactness checkable — in a CI matrix with no ONNX
//! Runtime present. The `neural` module re-exposes it through the
//! `wai.neural.int_synth` capability.
//!
//! Quality needs a quantization-aware-trained model (the weights are integers);
//! producing that model is a tooling step (`tools/wai_int_synth_export.py`). The
//! *decode contract* — the thing that makes cross-hardware bit-exactness real —
//! lives here and is exact regardless.
//!
//! Wire format (little-endian):
//! u32 H, u32 W, u16 C, u8 S_log2, u8 _reserved (12 B)
//! C*S*S bytes int8 latents, channel-major
//!
//! Sink file (next to the registered model path): `int_synth.json`
//! { "layers": [ { in_ch, out_ch, k, stride, pad, out_pad,
//! weight:[i32; in*out*k*k] (layout ic,oc,ky,kx),
//! bias:[i32; out_ch], shift:u32, relu:bool }, … ] }
//! The final layer MUST output 3 channels at H×W; values are clamped to [0,255].
use std::path::Path;
use serde::Deserialize;
const HEADER: usize = 12;
/// Decode error for the integer synthesis path. Deliberately self-contained so
/// the decoder carries no dependency on the `ort`-backed `neural` module; the
/// `neural` dispatch maps this onto its own `DecodeError::InvalidPayload`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IntSynthError {
InvalidPayload(String),
}
impl std::fmt::Display for IntSynthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidPayload(e) => write!(f, "invalid payload: {e}"),
}
}
}
impl std::error::Error for IntSynthError {}
/// Decoded image: RGB row-major u8, H × W × 3 — the integer-exact output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IntImage {
pub rgb: Vec<u8>,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Deserialize)]
struct LayerSpec {
in_ch: usize,
out_ch: usize,
k: usize,
stride: usize,
pad: usize,
#[serde(default)]
out_pad: usize,
/// Integer weights, layout `[in_ch][out_ch][k][k]` (transposed-conv order).
weight: Vec<i32>,
/// Integer bias, `[out_ch]`.
bias: Vec<i32>,
/// Arithmetic right-shift applied after accumulate+bias (requantize).
#[serde(default)]
shift: u32,
/// Integer ReLU (clamp at 0) after requantize.
#[serde(default)]
relu: bool,
}
#[derive(Debug, Clone, Deserialize)]
struct SynthSpec {
layers: Vec<LayerSpec>,
}
/// One integer transposed convolution + bias + requantize + optional ReLU.
/// `input` is `[in_ch, hin, win]` row-major `i64`. Returns `([out_ch, hout,
/// wout] i64, hout, wout)`. Pure integer arithmetic — the whole point.
fn conv_transpose2d(
input: &[i64],
cin: usize,
hin: usize,
win: usize,
l: &LayerSpec,
) -> Result<(Vec<i64>, usize, usize), IntSynthError> {
let (k, s, p, op) = (l.k, l.stride, l.pad, l.out_pad);
let hout_i = (hin as i64 - 1) * s as i64 + k as i64 + op as i64 - 2 * p as i64;
let wout_i = (win as i64 - 1) * s as i64 + k as i64 + op as i64 - 2 * p as i64;
if hout_i <= 0 || wout_i <= 0 {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth layer produces non-positive output {hout_i}x{wout_i}")));
}
let (hout, wout) = (hout_i as usize, wout_i as usize);
if l.weight.len() != cin * l.out_ch * k * k {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth weight len {} != {}*{}*{}*{}", l.weight.len(), cin, l.out_ch, k, k)));
}
if l.bias.len() != l.out_ch {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth bias len {} != out_ch {}", l.bias.len(), l.out_ch)));
}
let mut out = vec![0i64; l.out_ch * hout * wout];
for ic in 0..cin {
for iy in 0..hin {
for ix in 0..win {
let v = input[(ic * hin + iy) * win + ix];
if v == 0 {
continue;
}
for oc in 0..l.out_ch {
let wbase = (ic * l.out_ch + oc) * k * k;
let obase = oc * hout * wout;
for ky in 0..k {
let ty = iy * s + ky;
if ty < p {
continue;
}
let oy = ty - p;
if oy >= hout {
continue;
}
for kx in 0..k {
let tx = ix * s + kx;
if tx < p {
continue;
}
let ox = tx - p;
if ox >= wout {
continue;
}
out[obase + oy * wout + ox] +=
l.weight[wbase + ky * k + kx] as i64 * v;
}
}
}
}
}
}
// Bias, requantize (arithmetic shift — deterministic), integer ReLU.
for oc in 0..l.out_ch {
let b = l.bias[oc] as i64;
let base = oc * hout * wout;
for v in &mut out[base..base + hout * wout] {
let mut x = (*v + b) >> l.shift;
if l.relu && x < 0 {
x = 0;
}
*v = x;
}
}
Ok((out, hout, wout))
}
/// Decode an int_synth payload against the `int_synth.json` sibling of
/// `model_path`. Byte-identical on every machine — the `replicate` contract.
///
/// Filesystem-only convenience: reads the spec and delegates to
/// [`decode_from_spec`], which is the entry point with no `std::fs` (the one a
/// WASM / no-filesystem sink calls with the spec bytes supplied by the host).
pub fn decode(payload: &[u8], model_path: &Path) -> Result<IntImage, IntSynthError> {
let dir = model_path.parent()
.ok_or_else(|| IntSynthError::InvalidPayload("model path has no parent".into()))?;
let spec_path = dir.join("int_synth.json");
let raw = std::fs::read(&spec_path)
.map_err(|e| IntSynthError::InvalidPayload(format!("{}: {e}", spec_path.display())))?;
decode_from_spec(payload, &raw)
}
/// Decode an int_synth payload against the model spec supplied as bytes (the
/// `int_synth.json` contents). No filesystem, no float, no ONNX — the
/// integer-exact decode a browser / WASM sink runs directly, host-supplied
/// weights in, RGB out. Byte-identical on every machine.
pub fn decode_from_spec(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, IntSynthError> {
if payload.len() < HEADER {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth payload {} B < {HEADER} B header", payload.len())));
}
let h = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
let w = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
let c = u16::from_le_bytes(payload[8..10].try_into().unwrap()) as usize;
let s = 1usize << payload[10];
let n = c * s * s;
if payload.len() != HEADER + n {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth payload {} != {HEADER} + C*S*S={n}", payload.len())));
}
let spec: SynthSpec = serde_json::from_slice(spec_json)
.map_err(|e| IntSynthError::InvalidPayload(format!("int_synth.json parse: {e}")))?;
// int8 latents → i64.
let mut cur: Vec<i64> = payload[HEADER..].iter().map(|&b| b as i8 as i64).collect();
let (mut cin, mut ch, mut cw) = (c, s, s);
for (i, l) in spec.layers.iter().enumerate() {
if l.in_ch != cin {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth layer {i} in_ch {} != current {cin}", l.in_ch)));
}
let (out, ho, wo) = conv_transpose2d(&cur, cin, ch, cw, l)?;
cur = out;
cin = l.out_ch;
ch = ho;
cw = wo;
}
if cin != 3 || ch != h || cw != w {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth output [{cin},{ch},{cw}] != [3,{h},{w}]")));
}
let mut rgb = vec![0u8; h * w * 3];
let hw = h * w;
for y in 0..h {
for x in 0..w {
let pi = y * w + x;
for ch_i in 0..3 {
rgb[pi * 3 + ch_i] = cur[ch_i * hw + pi].clamp(0, 255) as u8;
}
}
}
Ok(IntImage { rgb, width: w as u32, height: h as u32 })
}
#[cfg(test)]
mod tests {
use super::*;
fn layer(out_ch: usize, in_ch: usize, k: usize, stride: usize, weight: Vec<i32>) -> LayerSpec {
LayerSpec {
in_ch, out_ch, k, stride, pad: 0, out_pad: 0,
weight, bias: vec![0; out_ch], shift: 0, relu: false,
}
}
#[test]
fn transposed_conv_matches_hand_computation() {
// 1 in-channel, 1 out-channel, 2×2 kernel [[1,2],[3,4]], stride 2.
// input [[5]] (1×1) scatters to a 2×2 output = kernel * 5.
let l = layer(1, 1, 2, 2, vec![1, 2, 3, 4]);
let (out, ho, wo) = conv_transpose2d(&[5], 1, 1, 1, &l).unwrap();
assert_eq!((ho, wo), (2, 2));
assert_eq!(out, vec![5, 10, 15, 20]);
}
#[test]
fn requantize_shift_and_relu_are_integer_exact() {
// shift=1 halves (arithmetic), relu clamps negatives.
let mut l = layer(1, 1, 1, 1, vec![1]);
l.shift = 1;
l.relu = true;
l.bias = vec![0];
// input -3 → weight*in = -3 → >>1 = -2 → relu → 0.
let (out, _, _) = conv_transpose2d(&[-3], 1, 1, 1, &l).unwrap();
assert_eq!(out, vec![0]);
// input 7 → 7 → >>1 = 3.
let (out, _, _) = conv_transpose2d(&[7], 1, 1, 1, &l).unwrap();
assert_eq!(out, vec![3]);
}
#[test]
fn decode_is_deterministic_by_construction() {
// Two runs of the pure integer op on the same input are byte-identical
// (the property that makes this a sound `replicate` member).
let l = layer(2, 1, 3, 2, (0..18).collect());
let input = [3, -4, 5];
let a = conv_transpose2d(&input, 1, 1, 3, &l).unwrap();
let b = conv_transpose2d(&input, 1, 1, 3, &l).unwrap();
assert_eq!(a, b);
}
/// FULL decode path, no ONNX: a constructed 1-layer integer model + a tiny
/// latent → exact RGB, and byte-identical across runs. This validates the
/// integer-exact `replicate` member end to end (header parse, latent unpack,
/// conv, requantize, clamp, RGB layout).
#[test]
fn end_to_end_decode_is_exact_and_deterministic() {
let dir = std::env::temp_dir().join("wai_int_synth_e2e");
std::fs::create_dir_all(&dir).unwrap();
// 1 layer, in_ch=1 → out_ch=3 (RGB), 2×2 kernel, stride 2. Each output
// channel has a constant kernel (1, 2, 3), so a 1×1 latent of 10 scatters
// to a 2×2 image where every pixel is (10, 20, 30).
let weight: Vec<i32> = (1..=3).flat_map(|oc| std::iter::repeat(oc).take(4)).collect();
let spec = serde_json::json!({"layers": [{
"in_ch": 1, "out_ch": 3, "k": 2, "stride": 2, "pad": 0, "out_pad": 0,
"weight": weight, "bias": [0, 0, 0], "shift": 0, "relu": false,
}]});
std::fs::write(dir.join("int_synth.json"), serde_json::to_vec(&spec).unwrap()).unwrap();
let model_path = dir.join("model");
let mut payload = Vec::new();
payload.extend_from_slice(&2u32.to_le_bytes()); // H
payload.extend_from_slice(&2u32.to_le_bytes()); // W
payload.extend_from_slice(&1u16.to_le_bytes()); // C
payload.push(0); // S_log2 = 0 → S = 1
payload.push(0); // reserved
payload.push(10u8); // one int8 latent = 10
let img = decode(&payload, &model_path).expect("decode");
assert_eq!((img.width, img.height), (2, 2));
let expected: Vec<u8> = (0..4).flat_map(|_| [10u8, 20, 30]).collect();
assert_eq!(img.rgb, expected, "integer synthesis must hit the exact bytes");
// Byte-identical across runs — the replicate property.
assert_eq!(decode(&payload, &model_path).unwrap().rgb, img.rgb);
let _ = std::fs::remove_dir_all(&dir);
}
}