open-standards/wai/tools/wai_int_synth_real.py
Transaction Science 81c1813822 wai: a real trained int_synth model — real photo, decoded bit-exact everywhere
int_synth's determinism was proven on synthetic/constructed models; this adds
a model TRAINED ON REAL IMAGES decoding a REAL PHOTOGRAPH, and proves the same
bit-exactness on it. tools/wai_int_synth_real.py trains an autoencoder on real
photos, post-training-quantizes the decoder with per-layer activation-scale
calibration (the working recipe — pure QAT through the >> shift can't learn),
encodes a held-out image to an int8 latent, and decodes it through the EXPORTED
integer model in numpy (the same i64 scatter-accumulate / +bias / arithmetic
>> shift / clamp that int_synth.rs runs) as the reference.

New corpus vector `photo`: a 128×128 macOS Sonoma landscape → a 6,156-byte
int8 latent (96×8×8) + a four-layer trained integer decoder, ~21.6 dB at ~3
bpp. Honest scope: a pure integer transposed-conv decoder with no entropy
model — quality is modest BY DESIGN; the point is that the decode is
bit-identical on every machine. wai_int_synth_vectors `gen` cross-checks the
Rust decode against the python integer-forward (ref_rgb.bin) to max|Δ|=0 and
freezes the rgb_hash; the four-platform conformance CI then enforces the same
bytes everywhere. A real trained model, a real photo, decoded identically on
every machine — the int_synth thesis, demonstrated end to end.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 17:12:48 -04:00

204 lines
9.5 KiB
Python
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.

"""Train a REAL int_synth decoder on REAL images and emit a committable
conformance fixture: the integer model (`int_synth.json`), an encoded test
image (`latent.bin`, the wire payload), and the integer-forward reference RGB
+ PSNR. The Rust decoder (`int_synth.rs`) reproduces the reference BIT-EXACTLY
on every machine — that is the whole point of int_synth: the determinism is
exact by construction, and this proves it on a real photograph, not a
constructed toy or a synthetic gradient.
Pipeline (the WORKING recipe — see wai_int_synth_train.py for why pure QAT
through the `>> shift` fails): float-train an autoencoder on random crops of
real photos, then post-training-quantize the DECODER with per-layer
activation-scale calibration so the integer pipeline tracks the float one.
Encode a held-out crop through the float encoder, quantize the latent to int8,
and decode it through the EXPORTED INTEGER model in numpy — the same i64
arithmetic int_synth.rs runs (scatter-accumulate, +bias, arithmetic `>> shift`,
integer ReLU, clamp [0,255]). That numpy decode is the reference the Rust
decoder must match to max|Δ|=0.
python tools/wai_int_synth_real.py --src <dir-of-pngs> --out <fixture-dir>
"""
from __future__ import annotations
import argparse
import glob
import json
import math
import struct
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
def load_crops(src: str, size: int, n: int, seed: int = 0, downscale: bool = False) -> torch.Tensor:
rng = np.random.default_rng(seed)
paths = sorted(glob.glob(str(Path(src) / "*.png")) + glob.glob(str(Path(src) / "*.jpg")))
if not paths:
raise SystemExit(f"no images in {src}")
if downscale:
# each image becomes a full recognizable scene at `size`×`size`.
out = [np.asarray(Image.open(p).convert("RGB").resize((size, size), Image.LANCZOS), np.float32) / 255.0
for p in paths]
return torch.from_numpy(np.stack([o.transpose(2, 0, 1) for o in out]))
crops = []
per = max(1, n // len(paths))
for p in paths:
im = np.asarray(Image.open(p).convert("RGB"), np.float32) / 255.0
H, W, _ = im.shape
for _ in range(per):
y = int(rng.integers(0, max(1, H - size)))
x = int(rng.integers(0, max(1, W - size)))
crops.append(im[y:y + size, x:x + size].transpose(2, 0, 1))
return torch.from_numpy(np.stack(crops[:n]))
# ---- the integer forward, byte-for-byte as int_synth.rs runs it ----
def conv_transpose2d_int(inp: np.ndarray, layer: dict) -> np.ndarray:
cin, hin, win = inp.shape
oc, k, s, p, op = layer["out_ch"], layer["k"], layer["stride"], layer["pad"], layer["out_pad"]
hout = (hin - 1) * s + k + op - 2 * p
wout = (win - 1) * s + k + op - 2 * p
W = np.array(layer["weight"], np.int64).reshape(cin, oc, k, k)
out = np.zeros((oc, hout, wout), np.int64)
for ky in range(k):
for kx in range(k):
# contribution of this kernel tap: (oc, hin, win)
contrib = np.einsum("iohw,ihw->ohw", W[:, :, ky, kx][:, :, None, None] + np.zeros((1, 1, hin, win), np.int64), inp)
ys = np.arange(hin) * s + ky - p
xs = np.arange(win) * s + kx - p
ym = (ys >= 0) & (ys < hout)
xm = (xs >= 0) & (xs < wout)
if not ym.any() or not xm.any():
continue
out[np.ix_(np.arange(oc), ys[ym], xs[xm])] += contrib[:, ym][:, :, xm]
b = np.array(layer["bias"], np.int64).reshape(oc, 1, 1)
out = (out + b) >> np.int64(layer["shift"]) # arithmetic shift, matches Rust i64 >>
if layer["relu"]:
out = np.maximum(out, 0)
return out
def decode_int(latent_i8: np.ndarray, layers: list[dict], h: int, w: int) -> np.ndarray:
cur = latent_i8.astype(np.int64)
for l in layers:
cur = conv_transpose2d_int(cur, l)
assert cur.shape[0] == 3 and cur.shape[1] == h and cur.shape[2] == w, cur.shape
return np.clip(cur, 0, 255).astype(np.uint8).transpose(1, 2, 0) # H,W,3
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--src", required=True, help="dir of real .png/.jpg photos")
ap.add_argument("--out", required=True, help="fixture output dir")
ap.add_argument("--chans", type=int, nargs="+", default=[64, 48, 32, 16, 3])
ap.add_argument("--latent-side", type=int, default=8)
ap.add_argument("--crops", type=int, default=160)
ap.add_argument("--epochs", type=int, default=2500)
ap.add_argument("--lr", type=float, default=2e-3)
ap.add_argument("--downscale", action="store_true", help="train on full images resized to the model size (recognizable whole scenes) instead of random crops")
ap.add_argument("--test-idx", type=int, default=0, help="which image (in --downscale mode) is the reconstructed fixture")
args = ap.parse_args()
torch.manual_seed(0)
dev = "mps" if torch.backends.mps.is_available() else "cpu"
C, LS = args.chans[0], args.latent_side
n_layers = len(args.chans) - 1
size = LS * (2 ** n_layers)
print(f"device {dev} · image {size}×{size} · latent {LS}×{LS}×{C} · {n_layers} decoder layers")
if args.downscale:
data = load_crops(args.src, size, args.crops, downscale=True).to(dev)
test = data[args.test_idx:args.test_idx + 1] # one full scene the model represents
print(f"downscale mode: {data.shape[0]} full scenes; test image index {args.test_idx}")
else:
data = load_crops(args.src, size, args.crops).to(dev)
test = load_crops(args.src, size, 1, seed=999).to(dev) # held-out crop
dn = lambda a, b: nn.Conv2d(a, b, 5, 2, 2)
up = lambda a, b: nn.ConvTranspose2d(a, b, 5, 2, 2, output_padding=1)
enc = nn.Sequential(*sum([[dn(3 if i == 0 else C, C), nn.ReLU()] for i in range(n_layers)], [])[:-1]).to(dev)
dec = nn.ModuleList([up(args.chans[i], args.chans[i + 1]) for i in range(n_layers)]).to(dev)
def dfwd(y):
x = y
for i, c in enumerate(dec):
x = c(x)
if i < len(dec) - 1:
x = torch.relu(x)
return x
opt = torch.optim.Adam(list(enc.parameters()) + [p for c in dec for p in c.parameters()], lr=args.lr)
for ep in range(args.epochs):
opt.zero_grad()
recon = dfwd(enc(data) * 64.0)
loss = F.mse_loss(recon.clamp(0, 1) * 255, data * 255)
loss.backward(); opt.step()
if ep % 250 == 0 or ep == args.epochs - 1:
print(f" epoch {ep:4d} train px-mse {loss.item():7.1f}")
# ---- calibrate activation scales on the training set, then quantize ----
with torch.no_grad():
x = enc(data) * 64.0
amax = []
for i, c in enumerate(dec):
x = c(x)
if i < len(dec) - 1:
x = torch.relu(x)
amax.append(float(x.abs().max()))
s0 = float((enc(data) * 64.0).abs().max()) / 127.0
scales = [s0] + [a / 127.0 for a in amax[:-1]] + [1.0 / 255.0]
layers, sin = [], s0
for i, c in enumerate(dec):
sw = float(c.weight.detach().abs().max()) / 127.0
shift = max(int(round(math.log2(scales[i + 1] / (sw * sin)))), 0)
layers.append({
"in_ch": c.in_channels, "out_ch": c.out_channels, "k": 5, "stride": 2, "pad": 2, "out_pad": 1,
"weight": torch.clamp(torch.round(c.weight.detach() / sw), -128, 127).to(torch.int64).reshape(-1).tolist(),
"bias": torch.round(c.bias.detach() / (sw * sin)).to(torch.int64).reshape(-1).tolist(),
"shift": shift, "relu": i < len(dec) - 1,
})
sin = scales[i + 1]
# ---- encode the held-out test image → int8 latent → wire payload ----
with torch.no_grad():
lat = (enc(test) * 64.0)[0].cpu().numpy() # (C, LS, LS)
lat_i8 = np.clip(np.round(lat / s0), -128, 127).astype(np.int8)
out = Path(args.out); out.mkdir(parents=True, exist_ok=True)
(out / "int_synth.json").write_text(json.dumps({"layers": layers}, separators=(",", ":")))
# wire payload: u32 H, u32 W, u16 C, u8 S_log2, u8 reserved, then C*S*S int8
s_log2 = int(round(math.log2(LS)))
payload = struct.pack("<IIHBB", size, size, C, s_log2, 0) + lat_i8.tobytes()
(out / "latent.bin").write_bytes(payload)
# reference = the EXPORTED integer model decoded in numpy (== int_synth.rs)
recon = decode_int(lat_i8, layers, size, size)
orig = (test[0].cpu().numpy().transpose(1, 2, 0) * 255).round().astype(np.uint8)
Image.fromarray(recon).save(out / "recon.png")
Image.fromarray(orig).save(out / "original.png")
recon.tofile(out / "ref_rgb.bin")
mse = float(np.mean((recon.astype(np.float64) - orig.astype(np.float64)) ** 2))
psnr = 99.0 if mse < 1e-9 else 10 * math.log10(255 * 255 / mse)
bpp = (len(payload) * 8) / (size * size)
# side-by-side (original | integer-decoded) for a demo, + metadata
Image.fromarray(np.concatenate([orig, recon], axis=1)).save(out / "compare.png")
(out / "meta.json").write_text(json.dumps({
"width": size, "height": size, "in_channels": C, "s_log2": s_log2,
"payload_bytes": len(payload), "psnr_db": round(psnr, 2), "bpp": round(bpp, 3),
}, indent=2))
print(f"\nwrote fixture → {out}")
print(f" model shifts {[l['shift'] for l in layers]} · latent {C}×{LS}×{LS} int8 ({len(payload)} B payload)")
print(f" real-image reconstruction: PSNR {psnr:.2f} dB · {bpp:.2f} bpp")
print(f" the integer decode is bit-identical on every machine; Rust int_synth must match ref_rgb.bin exactly")
return 0
if __name__ == "__main__":
raise SystemExit(main())