Some checks failed
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 8s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 7s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 7s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 7s
wai cross-architecture byte-equality / does this change touch it (pull_request) Successful in 7s
WAI operation catalogue / does this change touch it (pull_request) Successful in 7s
WAI conformance / does this change touch it (pull_request) Successful in 13s
WAI energy receipts / does this change touch it (pull_request) Successful in 8s
wai-fleet service / does this change touch it (pull_request) Successful in 8s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 13s
wai formal verification (integer core) / does this change touch it (pull_request) Successful in 8s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 12s
wai-ledger service / does this change touch it (pull_request) Successful in 10s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 8s
WAI neural sink gates / does this change touch it (pull_request) Successful in 13s
wai-ops-npu integer operators on neural processors / does this change touch it (pull_request) Successful in 6s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 8s
wai-ops integer operators / does this change touch it (pull_request) Successful in 15s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 8s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 13s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 8s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 13s
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Successful in 8s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 8s
WAI Studio / does this change touch it (pull_request) Successful in 13s
wai-web wasm32 / does this change touch it (pull_request) Successful in 8s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m28s
wai-ops-npu integer operators on neural processors / neural processor (framework-128gb silicon) (pull_request) Successful in 13s
wai formal verification (integer core) / kani (isqrt512) (pull_request) Successful in 29m16s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 32s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m17s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 32s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Failing after 1m42s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 3m1s
wai formal verification (integer core) / kani (rans64-t2) (pull_request) Successful in 13m17s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 40s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m52s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m29s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 8m59s
wai formal verification (integer core) / every harness in one shard (pull_request) Successful in 24s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m28s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m44s
wai-fleet service / service · linux (pull_request) Successful in 2m5s
wai formal verification (integer core) / kani (stage) (pull_request) Successful in 3m0s
wai formal verification (integer core) / kani (plan) (pull_request) Successful in 5m52s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 8m12s
wai-ledger service / service · linux (pull_request) Successful in 1m38s
wai formal verification (integer core) / kani (integer-core) (pull_request) Successful in 5m39s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m45s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 5m56s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 3m4s
wai-ops integer operators / linux-x86_64 (pull_request) Successful in 50s
wai-ops integer operators / wasm32-wasip2 (wasmtime) (pull_request) Successful in 30s
wai-ops integer operators / wasm32 under Node (pull_request) Successful in 21s
wai-ops integer operators / independent checker (Python) (pull_request) Successful in 12s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 12m57s
wai-pack / wai-pack · linux (pull_request) Successful in 1m43s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 15m38s
WAI neural sink gates / the exact JPEG-AI decoder reports its path (pinned inputs, required) (pull_request) Successful in 5m23s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m28s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 2m48s
wai formal verification (integer core) / kani (sound) (pull_request) Successful in 13m40s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m2s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 2m40s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m28s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m24s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 5m11s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m30s
wai formal verification (integer core) / kani (stage-long) (pull_request) Successful in 20m33s
wai formal verification (integer core) / kani (arith) (pull_request) Successful in 25m17s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m9s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 57s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m16s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 3m34s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 30s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 22s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 7m7s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m17s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 11m55s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m49s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 37s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 35s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m24s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m11s
wai-fleet service / service · macos (pull_request) Successful in 2m5s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 9m26s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 12m23s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 12s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 34s
wai-ledger service / service · macos (pull_request) Successful in 1m8s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m20s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m8s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 20m55s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 6m35s
wai-ops-npu integer operators on neural processors / builds · no device is not a pass (linux-arm64) (pull_request) Successful in 21s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 16m58s
wai-ops integer operators / macos-arm64 (pull_request) Successful in 1m0s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m56s
wai-pack / wai-pack · macos (pull_request) Successful in 3m19s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 26s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 14s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 12m3s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 1m11s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 13m4s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m46s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 5m19s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 9m20s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 8m15s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 11m44s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 18m0s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h30m13s
The change of wai/neural-corpus-licence (44d5f2264..6bddde42d, 5 commits), on the fix branch: its changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files, corpus files and test tags, and every other file as the branch has it. - 40052a902 wai: the trained int_synth vector's tool generates its own pictures from a seed - c86d58210 wai: neural-conformance's photo, trained and coded on generated pictures - 4321d68cc wai: every neural-conformance vector has a licence record, and a lint requires one - 63aea099a wai: photo's licence record gives its training pictures as streams 1 to 512 - 6bddde42d wai: the trained int_synth vector's check holds its licence record to meta.json Co-Authored-By: Claude Opus 5.5 <[email protected]>
409 lines
20 KiB
Python
409 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
"""Train an int_synth decoder and emit the committable conformance vector `photo` of
|
||
`neural-conformance/`: the integer model (`int_synth.json`), a coded picture
|
||
(`latent.bin`, the wire payload), the integer-forward reference RGB (`ref_rgb.bin`) and
|
||
`meta.json`. The Rust decoder (`int_synth.rs`) reproduces the reference BIT-EXACTLY on
|
||
every machine: the determinism is exact by construction, and this vector shows it on a
|
||
model trained in float and quantised after training, not only on seeded weights.
|
||
|
||
**The pictures are generated here; the tool reads no image file.** The pictures the
|
||
decoder is trained on, and the picture the vector codes, are written by `picture()`
|
||
below from a seed: 128 × 128 RGB8 dead-leaves pictures. A picture starts as a vertical
|
||
gradient between two colours; then 400 discs are painted back to front, each with a
|
||
centre uniform over the picture, a radius ⌊32·16 / (16 + u)⌋ (at least 2) for u uniform
|
||
in 0..239, a colour each channel of which is uniform in 16..239, and a linear shading
|
||
⌊(gx·dx + gy·dy) / 8⌋ for gx, gy uniform in −3..3, where (dx, dy) is the pixel's offset
|
||
from the centre; then every sample gets noise uniform in −2..2, and every value is
|
||
clamped to 0..255. Every draw is a SplitMix64 counter stream keyed by (seed, stream),
|
||
and the generator is integer arithmetic only, so a seed gives the same pictures on
|
||
every machine and every Python. The coded picture is stream 0 of the seed and the
|
||
training pictures are streams 1 to N: the decoder never sees the picture it codes.
|
||
|
||
Pipeline (the working recipe; see wai_int_synth_train.py for why training through the
|
||
`>> shift` fails): float-train an autoencoder on the training pictures, with torch
|
||
seeded from the same seed, deterministic algorithms and a fixed thread count on the
|
||
CPU: Adam steps on batches drawn by a generator seeded from the same seed, the learning
|
||
rate falling to a tenth for the last quarter of the steps, on the squared error of the
|
||
decoder's output before the clamp, so that an output below 0 or above 1 still has a
|
||
gradient; then quantise the DECODER after training, with each layer's activation scale
|
||
measured on the training pictures (its largest magnitude over 127, and 1/255 at the
|
||
output), its shift the largest that keeps its weights within ±127, and its weight scale
|
||
the one that makes the shift carry that activation scale exactly, so the integer
|
||
pipeline tracks the float one. Encode
|
||
the coded picture through the float encoder, quantise 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 to 0..255).
|
||
That numpy decode is the reference the Rust decoder must match to max|Δ|=0.
|
||
|
||
python tools/wai_int_synth_real.py --out <dir> [--seed S] [--pictures N] [--steps K]
|
||
python tools/wai_int_synth_real.py --picture-only <out.ppm> [--seed S] [--stream K]
|
||
python tools/wai_int_synth_real.py --check <vector-dir>
|
||
|
||
Training needs numpy and torch. `--picture-only` writes one generated picture as a
|
||
binary PPM, and `--check` regenerates the picture a committed vector codes from the
|
||
seed in its `meta.json`, requires the vector's reference decode (`ref_rgb.bin`) to
|
||
reconstruct it at the PSNR `meta.json` records, and requires the vector's record in the
|
||
corpus's `licences.json` to state the seed and streams `meta.json` records; both need
|
||
only the Python standard library. The committed vector's command is in
|
||
`neural-conformance/README.md`.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import json
|
||
import math
|
||
import struct
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
SIDE = 128 # the pictures' side; the model's output size
|
||
DISCS = 400 # discs per picture
|
||
THREADS = 4 # fixed: CPU reductions depend on it
|
||
SEED = 20261104 # the committed vector's seed
|
||
PICTURES = 512 # training pictures: streams 1 to PICTURES
|
||
STEPS, BATCH, LR = 3000, 32, 2e-4
|
||
|
||
# ---- the generated pictures (integer arithmetic only) --------------------------------
|
||
M64 = (1 << 64) - 1
|
||
GOLDEN = 0x9E3779B97F4A7C15
|
||
|
||
|
||
def _mix64(x):
|
||
"""SplitMix64's finalizer."""
|
||
x &= M64
|
||
x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & M64
|
||
x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & M64
|
||
return x ^ (x >> 31)
|
||
|
||
|
||
class Rng:
|
||
"""A SplitMix64 stream, keyed by (seed, stream)."""
|
||
|
||
def __init__(self, seed, stream):
|
||
self.s = _mix64(seed * GOLDEN + stream * 0xD1B54A32D192ED03)
|
||
|
||
def next(self):
|
||
self.s = (self.s + GOLDEN) & M64
|
||
return _mix64(self.s)
|
||
|
||
def int(self, lo, hi):
|
||
"""Uniform integer in [lo, hi]."""
|
||
return lo + self.next() % (hi - lo + 1)
|
||
|
||
|
||
def picture(seed, stream, side=SIDE):
|
||
"""One dead-leaves picture as RGB8 bytes: rows top to bottom, pixels left to right,
|
||
R, G, B. The module docstring states the formula."""
|
||
r = Rng(seed, stream)
|
||
top = [r.int(16, 239) for _ in range(3)]
|
||
bottom = [r.int(16, 239) for _ in range(3)]
|
||
px = bytearray(side * side * 3)
|
||
for y in range(side):
|
||
row = [(top[c] * (side - 1 - y) + bottom[c] * y) // (side - 1) for c in range(3)]
|
||
for x in range(side):
|
||
p = (y * side + x) * 3
|
||
px[p:p + 3] = bytes(row)
|
||
rmax = side // 4
|
||
for _ in range(DISCS):
|
||
cx, cy = r.int(0, side - 1), r.int(0, side - 1)
|
||
rad = max(2, rmax * 16 // (16 + r.int(0, 239)))
|
||
base = [r.int(16, 239) for _ in range(3)]
|
||
gx, gy = r.int(-3, 3), r.int(-3, 3)
|
||
for y in range(max(0, cy - rad), min(side, cy + rad + 1)):
|
||
dy = y - cy
|
||
for x in range(max(0, cx - rad), min(side, cx + rad + 1)):
|
||
dx = x - cx
|
||
if dx * dx + dy * dy > rad * rad:
|
||
continue
|
||
shade = (gx * dx + gy * dy) // 8
|
||
p = (y * side + x) * 3
|
||
for c in range(3):
|
||
px[p + c] = min(255, max(0, base[c] + shade))
|
||
for i in range(len(px)):
|
||
px[i] = min(255, max(0, px[i] + r.int(-2, 2)))
|
||
return bytes(px)
|
||
|
||
|
||
def write_ppm(path, rgb, w, h):
|
||
Path(path).write_bytes(b"P6\n%d %d\n255\n" % (w, h) + bytes(rgb))
|
||
|
||
|
||
def psnr_db(a, b):
|
||
"""PSNR of two RGB8 buffers, in dB, from their integer sum of squared differences."""
|
||
assert len(a) == len(b) and len(a) > 0
|
||
sse = sum((x - y) * (x - y) for x, y in zip(a, b))
|
||
return 99.0 if sse == 0 else 10 * math.log10(255 * 255 * len(a) / sse)
|
||
|
||
|
||
# ---- the integer forward, byte-for-byte as int_synth.rs runs it ----
|
||
def conv_transpose2d_int(inp, layer):
|
||
import numpy as np
|
||
|
||
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, layers, h, w):
|
||
import numpy as np
|
||
|
||
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
|
||
|
||
|
||
GENERATOR = "tools/wai_int_synth_real.py" # this tool, as a licence record names it
|
||
|
||
|
||
def licence_record_fails(vdir, meta):
|
||
"""The vector's record in the corpus's `licences.json` (two directories up) states
|
||
the pictures meta.json says the vector was made from: content generated by this
|
||
tool, the coded picture `seed S, stream K`, and the training pictures
|
||
`seed S, streams A to B` from meta.json's `training_streams`, a range that leaves
|
||
out the coded picture's stream."""
|
||
fails = []
|
||
seed, stream = meta["seed"], meta["stream"]
|
||
tr_streams = meta.get("training_streams")
|
||
if not (isinstance(tr_streams, list) and len(tr_streams) == 2
|
||
and all(isinstance(x, int) for x in tr_streams) and tr_streams[0] <= tr_streams[1]):
|
||
return [f"meta.json's training_streams {tr_streams!r} is not [first, last]"]
|
||
a, b = tr_streams
|
||
if a <= stream <= b:
|
||
fails.append(f"meta.json's training streams {a} to {b} include the coded picture's stream {stream}")
|
||
try:
|
||
records = json.loads((vdir.parent.parent / "licences.json").read_text())["vectors"]
|
||
rec = records[vdir.name]
|
||
tr = rec["training"]
|
||
except (OSError, ValueError, KeyError, TypeError) as e:
|
||
return fails + [f"licences.json has no record with a training record for {vdir.name} ({e!r})"]
|
||
want = {
|
||
"": f"the dead-leaves picture the tool generates from seed {seed}, stream {stream}",
|
||
"training ": f"the dead-leaves pictures the tool generates from seed {seed}, streams {a} to {b}",
|
||
}
|
||
for label, r in (("", rec), ("training ", tr)):
|
||
if not isinstance(r, dict):
|
||
fails.append(f"licences.json's {label}record for {vdir.name} is not an object")
|
||
continue
|
||
if r.get("content") != "generated" or r.get("generator") != GENERATOR:
|
||
fails.append(f"licences.json's {label}record for {vdir.name} does not say its content is "
|
||
f"generated by {GENERATOR}")
|
||
if r.get("input") != want[label]:
|
||
fails.append(f"licences.json's {label}input for {vdir.name} is {r.get('input')!r}; "
|
||
f"meta.json says {want[label]!r}")
|
||
return fails
|
||
|
||
|
||
def check(vdir):
|
||
"""The committed vector codes the picture its seed generates: its reference decode
|
||
(`ref_rgb.bin`, whose SHA-256 is the corpus's digest for the vector in
|
||
`sha256.json` two directories up) reconstructs that picture at the PSNR meta.json
|
||
records, to two decimals; and the vector's licence record states the pictures
|
||
meta.json says it was made from (`licence_record_fails`)."""
|
||
vdir = Path(vdir)
|
||
meta = json.loads((vdir / "meta.json").read_text())
|
||
w, h = meta["width"], meta["height"]
|
||
head = (vdir / "latent.bin").read_bytes()[:12]
|
||
ph, pw, _c, _s, _r = struct.unpack("<IIHBB", head)
|
||
ref = (vdir / "ref_rgb.bin").read_bytes()
|
||
digests = json.loads((vdir.parent.parent / "sha256.json").read_text())
|
||
fails = []
|
||
if (w, h) != (SIDE, SIDE):
|
||
fails.append(f"meta.json says {w}x{h}, not the generator's {SIDE}x{SIDE}")
|
||
if (ph, pw) != (h, w):
|
||
fails.append(f"latent.bin's header says {pw}x{ph}, meta.json {w}x{h}")
|
||
if len(ref) != w * h * 3:
|
||
fails.append(f"ref_rgb.bin holds {len(ref)} bytes, not {w * h * 3}")
|
||
want = digests.get(vdir.name, {}).get("sha256")
|
||
if hashlib.sha256(ref).hexdigest() != want:
|
||
fails.append(f"ref_rgb.bin's SHA-256 is not sha256.json's {vdir.name} digest ({want})")
|
||
if not fails:
|
||
src = picture(meta["seed"], meta["stream"], w)
|
||
got = round(psnr_db(src, ref), 2)
|
||
print(f"{vdir}: seed {meta['seed']} stream {meta['stream']}: the reference decode reconstructs it at {got:.2f} dB")
|
||
if got != meta["psnr_db"]:
|
||
fails.append(f"PSNR {got:.2f} dB against the generated picture, meta.json records {meta['psnr_db']}")
|
||
fails += licence_record_fails(vdir, meta)
|
||
for f in fails:
|
||
print(f"FAIL {vdir}: {f}", file=sys.stderr)
|
||
return 1 if fails else 0
|
||
|
||
|
||
def train(args):
|
||
import numpy as np
|
||
import torch
|
||
import torch.nn as nn
|
||
import torch.nn.functional as F
|
||
|
||
torch.manual_seed(args.seed)
|
||
torch.use_deterministic_algorithms(True)
|
||
torch.set_num_threads(THREADS)
|
||
|
||
C, LS = args.chans[0], args.latent_side
|
||
n_layers = len(args.chans) - 1
|
||
size = LS * (2 ** n_layers)
|
||
assert size == SIDE, f"--latent-side and --chans give {size}x{size}; the pictures are {SIDE}x{SIDE}"
|
||
print(f"image {size}x{size} · latent {LS}x{LS}x{C} · {n_layers} decoder layers · seed {args.seed} · "
|
||
f"{args.pictures} training pictures · {args.steps} steps of {args.batch} · lr {args.lr} · {THREADS} threads")
|
||
|
||
def tensor(rgb):
|
||
a = np.frombuffer(rgb, np.uint8).reshape(size, size, 3).astype(np.float32) / 255.0
|
||
return a.transpose(2, 0, 1)
|
||
|
||
src = picture(args.seed, 0, size)
|
||
data = torch.from_numpy(np.stack([tensor(picture(args.seed, k, size)) for k in range(1, args.pictures + 1)]))
|
||
test = torch.from_numpy(tensor(src)[None])
|
||
|
||
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])
|
||
dec = nn.ModuleList([up(args.chans[i], args.chans[i + 1]) for i in range(n_layers)])
|
||
|
||
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)
|
||
# The learning rate falls to a tenth for the last quarter of the steps.
|
||
sched = torch.optim.lr_scheduler.MultiStepLR(opt, milestones=[args.steps * 3 // 4], gamma=0.1)
|
||
order = torch.Generator().manual_seed(args.seed)
|
||
for step in range(args.steps):
|
||
batch = data[torch.randperm(len(data), generator=order)[:args.batch]]
|
||
opt.zero_grad()
|
||
recon = dfwd(enc(batch) * 64.0)
|
||
# Before the clamp: clamped, an output that starts below 0 has no gradient and
|
||
# the decoder can stay black.
|
||
loss = F.mse_loss(recon * 255, batch * 255)
|
||
loss.backward()
|
||
opt.step()
|
||
sched.step()
|
||
if step % 250 == 0 or step == args.steps - 1:
|
||
print(f" step {step:5d} batch px-mse {loss.item():7.1f}", flush=True)
|
||
|
||
def rgb8(t):
|
||
return (t[0].clamp(0, 1) * 255).round().to(torch.uint8).permute(1, 2, 0).contiguous().numpy().tobytes()
|
||
|
||
# ---- calibrate activation scales on the training set, then quantise ----
|
||
with torch.no_grad():
|
||
print(f" float decode of the coded picture: PSNR {psnr_db(src, rgb8(dfwd(enc(test) * 64.0))):.2f} dB")
|
||
s0max, amax = 0.0, [0.0] * n_layers
|
||
for k in range(0, len(data), args.batch):
|
||
x = enc(data[k:k + args.batch]) * 64.0
|
||
s0max = max(s0max, float(x.abs().max()))
|
||
for i, c in enumerate(dec):
|
||
x = c(x)
|
||
if i < len(dec) - 1:
|
||
x = torch.relu(x)
|
||
amax[i] = max(amax[i], float(x.abs().max()))
|
||
s0 = s0max / 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):
|
||
# The layer's output is acc · sw · sin / 2^shift, so its scale is the calibrated
|
||
# one exactly when sw = sout / (sin · 2^shift). The shift is the largest that
|
||
# keeps every weight within ±127 under that sw. Rounding the shift alone would
|
||
# leave each layer's gain off by up to √2, and the errors would multiply down
|
||
# the stack into the output's brightness and contrast.
|
||
sout = scales[i + 1]
|
||
wmax = float(c.weight.detach().abs().max())
|
||
shift = math.floor(math.log2(sout * 127 / (wmax * sin)))
|
||
if shift < 0:
|
||
print(f" layer {i}: no shift >= 0 keeps the weights within ±127; clamping them")
|
||
shift = 0
|
||
sw = sout / (sin * 2 ** shift)
|
||
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 = sout
|
||
|
||
# ---- encode the coded picture → int8 latent → wire payload ----
|
||
with torch.no_grad():
|
||
lat = (enc(test) * 64.0)[0].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)
|
||
torch.save({"enc": enc.state_dict(), "dec": dec.state_dict()}, out / "float_model.pt")
|
||
(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).tobytes()
|
||
(out / "ref_rgb.bin").write_bytes(recon)
|
||
write_ppm(out / "original.ppm", src, size, size)
|
||
write_ppm(out / "recon.ppm", recon, size, size)
|
||
|
||
psnr = psnr_db(src, recon)
|
||
bpp = (len(payload) * 8) / (size * size)
|
||
(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),
|
||
"seed": args.seed, "stream": 0, "training_streams": [1, args.pictures],
|
||
"steps": args.steps, "batch": args.batch, "lr": args.lr, "threads": THREADS,
|
||
"torch": torch.__version__, "numpy": np.__version__,
|
||
}, indent=2) + "\n")
|
||
print(f"\nwrote the vector → {out}")
|
||
print(f" model shifts {[l['shift'] for l in layers]} · latent {C}x{LS}x{LS} int8 ({len(payload)} B payload)")
|
||
print(f" reconstruction of the coded picture: PSNR {psnr:.2f} dB · {bpp:.2f} bpp")
|
||
return 0
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser(description="Train an int_synth decoder on generated pictures and write the corpus vector.")
|
||
ap.add_argument("--out", help="output dir: int_synth.json, latent.bin, ref_rgb.bin and meta.json (the vector), "
|
||
"two previews and the float model (float_model.pt)")
|
||
ap.add_argument("--seed", type=int, default=SEED)
|
||
ap.add_argument("--pictures", type=int, default=PICTURES, help="training pictures: streams 1 to N of the seed")
|
||
ap.add_argument("--chans", type=int, nargs="+", default=[96, 72, 48, 24, 3])
|
||
ap.add_argument("--latent-side", type=int, default=8)
|
||
ap.add_argument("--steps", type=int, default=STEPS, help="Adam steps, each on a batch drawn from the training pictures")
|
||
ap.add_argument("--batch", type=int, default=BATCH)
|
||
ap.add_argument("--lr", type=float, default=LR)
|
||
ap.add_argument("--picture-only", metavar="OUT.ppm", help="write one generated picture and exit")
|
||
ap.add_argument("--stream", type=int, default=0, help="with --picture-only: 0 the coded picture, 1 to N the training pictures")
|
||
ap.add_argument("--check", metavar="VECTOR_DIR", help="check that a committed vector codes the picture its seed generates")
|
||
args = ap.parse_args()
|
||
if args.check:
|
||
return check(args.check)
|
||
if args.picture_only:
|
||
write_ppm(args.picture_only, picture(args.seed, args.stream), SIDE, SIDE)
|
||
return 0
|
||
if not args.out:
|
||
ap.error("--out is required unless --picture-only or --check is given")
|
||
return train(args)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|