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
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]>
454 lines
21 KiB
Rust
454 lines
21 KiB
Rust
//! 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);
|
||
}
|
||
}
|