open-standards/wai/wai-rs/src/int_synth.rs
Transaction Science 4d7b9e28fd
All checks were successful
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m4s
wai-ops differential (nightly) / differential · debug (pull_request) Successful in 2m26s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 5s
wai-ops differential (nightly) / differential · release (pull_request) Successful in 3m4s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m54s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m32s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 5s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
wai cross-architecture byte-equality / does this change touch it (pull_request) Successful in 4s
WAI conformance / does this change touch it (pull_request) Successful in 4s
WAI energy receipts / does this change touch it (pull_request) Successful in 5s
wai formal verification (integer core) / does this change touch it (pull_request) Successful in 5s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 4s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 23s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 4s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 29s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 50s
WAI neural sink gates / does this change touch it (pull_request) Successful in 5s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 5s
wai-ops integer operators / does this change touch it (pull_request) Successful in 6s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 4s
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Successful in 4s
wai-web wasm32 / does this change touch it (pull_request) Successful in 5s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 29s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 17s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m10s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 30s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 2m55s
wai formal verification (integer core) / every harness in one shard (pull_request) Successful in 11s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 9m31s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 23s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 10m41s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 52s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 12s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 11m24s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 1m42s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 27s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 58s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 11m47s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 2m11s
wai formal verification (integer core) / kani (arith) (pull_request) Successful in 10m15s
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 20m20s
wai formal verification (integer core) / kani (plan) (pull_request) Successful in 7m49s
wai formal verification (integer core) / kani (rans64-t2) (pull_request) Successful in 14m52s
wai-pack / wai-pack · macos (pull_request) Successful in 1m20s
wai-ops integer operators / macos-arm64 (pull_request) Successful in 1m7s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 5m13s
wai formal verification (integer core) / kani (sound) (pull_request) Successful in 11m45s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 1m50s
wai formal verification (integer core) / kani (stage) (pull_request) Successful in 3m13s
wai formal verification (integer core) / kani (isqrt512) (pull_request) Successful in 49m3s
wai formal verification (integer core) / kani (stage-long) (pull_request) Successful in 21m19s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 46m48s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 47m39s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 46m52s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m28s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m20s
wai-pack / wai-pack · linux (pull_request) Successful in 1m19s
wai-ops integer operators / linux-x86_64 (pull_request) Successful in 52s
wai formal verification (integer core) / kani (integer-core) (pull_request) Successful in 6m8s
wai-ops integer operators / wasm32 under Node (pull_request) Successful in 22s
wai-ops integer operators / wasm32-wasip2 (wasmtime) (pull_request) Successful in 42s
wai-ops integer operators / independent checker (Python) (pull_request) Successful in 17s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 6m1s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 4s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 6m51s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 6m18s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m7s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m11s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 3m6s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m20s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m49s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 23s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 1m8s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m17s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m17s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m6s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 5m35s
wai: the store stays out of the sources the byte-equality crate includes
byte-exact-conformance #[path]-includes int_synth.rs and int_hyper_synth.rs, and
has no int_set_cache, codecs or pin module, so gate.sh's byte-exact step failed to
compile. int_synth::decode_from_spec is now three steps (check_payload,
load_spec, decode_loaded) and loads its set every time, as before the store;
int_payload's wai.neural.int_synth decode takes the middle step from the store.
wai-web's decodeIntSynth decodes through decode_int, as the other payload entry
points do (a refusal's message now starts with its code). The Resident impl for
int_hyper_synth::ops::Nets moves into int_set_cache.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 11:51:01 -04:00

454 lines
21 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**, whose round-off
//! depends on the kernels, accumulation order and fused operations the runtime
//! picks for each platform. 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 depends on how the integer weights were made, which is a tooling step
//! outside the decode contract. The reference tools train in float, then quantise
//! after training, deriving each layer's shift from its measured activation range
//! (`tools/wai_int_synth_train.py`, `tools/wai_int_synth_real.py`); the trained
//! model in the conformance corpus was made that way. The *decode contract* — the
//! thing that makes cross-hardware bit-exactness real — lives here and is exact
//! whatever the weights.
//!
//! Wire format (little-endian), `extensions/integer-payloads.md` §13:
//! 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].
//!
//! The decode ([`decode_from_spec`]) is the graph `synth` of the set's lowering
//! `int_synth/1` (`extensions/integer-operators.md` §13.1), loaded under `acc64` with its
//! bounds certificate and run through the integer operator set ([`crate::int_ops`]),
//! whose interpreter lends the transposed convolutions to the fast kernels. The direct
//! decode it replaced is kept, test-only (feature `legacy_int`), as `legacy`: each
//! layer's transposed convolution, bias, shift and ReLU computed here by the scalar
//! kernel. The differential tests decode random sets and payloads both ways
//! (`tests/int_ops_differential.rs`).
use std::path::Path;
#[cfg(any(test, feature = "legacy_int"))]
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,
}
#[cfg(any(test, feature = "legacy_int"))]
#[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,
}
#[cfg(any(test, feature = "legacy_int"))]
#[derive(Debug, Clone, Deserialize)]
struct SynthSpec {
layers: Vec<LayerSpec>,
}
/// The direct decode's transposed convolution, kept verbatim: the scalar kernel its fast
/// dispatch was checked against. That dispatch, and the guard that kept it to sizes this
/// kernel's own arithmetic forms, are gone: the decode runs through the integer operator
/// set, whose plan forms every size in checked arithmetic under its caps.
#[cfg(any(test, feature = "legacy_int"))]
mod reference {
use super::{IntSynthError, 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.
pub(super) 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_from_spec`] under the name `int_payload` and the browser crate use for
/// the byte-input entry of every integer decoder: the payload and the parameter
/// set's one file, `int_synth.json`. A pin over the set is verified before this is
/// called (`pin::verify_bytes`, `int_payload::resolve`), so it takes no pin.
pub fn decode_bytes(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, IntSynthError> {
decode_from_spec(payload, spec_json)
}
/// 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.
///
/// It decodes through the integer operator set, in three steps a sink may also take
/// one by one: [`check_payload`], [`load_spec`] and [`decode_loaded`]. The payload is
/// checked first (`extensions/integer-payloads.md` §13): its header, a reserved byte of
/// 0, `C ≥ 1`, `C·S·S ≤ 2^26` and its exact length. Then the set is lowered
/// (integer-operators §13.1, whose checks include at least one layer, fields below
/// `2^32`, `out_ch`, `k` and `stride` at least 1 and a shift of at most 127) and loaded
/// under `acc64`: load steps 1–8, the last being the bounds certificate
/// ([`crate::int_ops`]). Then the graph runs on the latents, through the operator set's
/// run-time plan with its caps, and its output must be `[3, H, W]`. Every refusal is
/// [`IntSynthError::InvalidPayload`] (`payload_malformed`). A sink that keeps loaded
/// sets between decodes answers the middle step from its store (`int_payload`'s decode
/// of `wai.neural.int_synth`).
pub fn decode_from_spec(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, IntSynthError> {
check_payload(payload)?;
let g = load_spec(spec_json)?;
decode_loaded(payload, &g)
}
/// The payload's checks, the first step of [`decode_from_spec`].
pub fn check_payload(payload: &[u8]) -> Result<(), IntSynthError> {
header(payload).map(|_| ())
}
fn refused(what: &str, r: wai_ops::Refusal) -> IntSynthError {
IntSynthError::InvalidPayload(format!("int_synth {what}: {} at {:?}", r.code.as_str(), r.at))
}
/// The set `int_synth.json` lowered by `int_synth/1` and loaded under `acc64`, the
/// second step of [`decode_from_spec`].
pub fn load_spec(spec_json: &[u8]) -> Result<wai_ops::Loaded, IntSynthError> {
let set = wai_ops::lower::int_synth(spec_json).map_err(|r| refused("int_synth.json does not lower", r))?;
crate::int_ops::load(set.get("synth").expect("int_synth/1 lowers to the graph synth")).map_err(|r| refused("set does not load", r))
}
/// The decode with its set loaded ([`load_spec`]), the last step of
/// [`decode_from_spec`]: the payload's checks again, then the graph on the latents.
pub fn decode_loaded(payload: &[u8], g: &wai_ops::Loaded) -> Result<IntImage, IntSynthError> {
let (h, w, c, s) = header(payload)?;
let latents: Vec<i64> = payload[HEADER..].iter().map(|&b| b as i8 as i64).collect();
let out = crate::int_ops::run1(g, wai_ops::Tensor::new(c, s, s, latents), &[]).map_err(|r| refused("decode", r))?;
if (out.c, out.h as usize, out.w as usize) != (3, h, w) {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth output [{},{},{}] != [3,{h},{w}]", out.c, out.h, out.w)));
}
// `clamp(0, 255)` ends the graph, so every value is a byte.
let hw = h * w;
let mut rgb = vec![0u8; hw * 3];
for (k, &v) in out.data.iter().enumerate() {
rgb[(k % hw) * 3 + k / hw] = v as u8;
}
Ok(IntImage { rgb, width: w as u32, height: h as u32 })
}
/// The payload's header (`extensions/integer-payloads.md` §13): `(H, W, C, S)`, after
/// its checks. The side is `2^S_log2`; `C·S·S` is formed without overflow and is at
/// most `2^26`, and the payload is exactly `12 + C·S·S` bytes.
fn header(payload: &[u8]) -> Result<(usize, usize, u32, u32), 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;
if payload[11] != 0 {
return Err(IntSynthError::InvalidPayload("int_synth payload's reserved byte is not 0".into()));
}
if c == 0 {
return Err(IntSynthError::InvalidPayload("int_synth payload has no latent channels".into()));
}
const CAP: usize = 1 << 26;
let n = 1usize.checked_shl(payload[10] as u32)
.and_then(|s| c.checked_mul(s)?.checked_mul(s).map(|n| (s, n)))
.filter(|&(_, n)| n <= CAP);
let Some((s, _)) = n.filter(|&(_, n)| HEADER.checked_add(n) == Some(payload.len())) else {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth payload {} B is not {HEADER} + C·S·S with C = {c}, S = 2^{}, C·S·S ≤ 2^26", payload.len(), payload[10])));
};
Ok((h, w, c as u32, s as u32))
}
/// The direct decode, as the decoder ran before it decoded through the integer operator
/// set: the payload and set read here, each layer's transposed convolution, bias, shift
/// and ReLU computed by the scalar reference kernel. 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::*;
/// The direct decode of [`super::decode_from_spec`]'s arguments.
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;
// The side is 2^payload[10]. A shift of the word's width or more panics with
// overflow checks on and masks without them, so the same bytes would decode two
// ways; and C·S·S must be formed without overflow. Neither can match a payload's
// length when it overflows, so both are refused here as a length mismatch.
let n = 1usize.checked_shl(payload[10] as u32)
.and_then(|s| c.checked_mul(s)?.checked_mul(s).map(|n| (s, n)));
let Some((s, _)) = n.filter(|&(_, n)| HEADER.checked_add(n) == Some(payload.len())) else {
return Err(IntSynthError::InvalidPayload(format!(
"int_synth payload {} B is not {HEADER} + C·S·S with C = {c}, S = 2^{}", payload.len(), payload[10])));
};
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) = reference::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::*;
// The direct decode's scalar kernel, against hand computation.
use super::reference::conv_transpose2d;
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);
}
}