#!/usr/bin/env python3 """What exactness costs `wai.neural.int_hyper`: the measurement behind research/determinism-cost.md and its record, research/determinism-cost/record.json. The float codec is the pretrained mean-scale hyperprior `mbt2018_mean` (MSE, qualities 1-8) as its own library codes and decodes it. The integer codec is the same model exported the way tools/wai_sota_int_synth_export.py exports quality 6 (per-channel int12 weights, activations at 2^12, integer IGDN, the integer hyper-decoder deciding every scale bucket), coded as a `WIH1` payload (extensions/integer-payloads.md §4) and decoded by the reference decoder (`wai_det_cost`, which runs `int_payload::decode_int`). Every step writes files under one work directory, so each can be rerun alone and on another machine. wai_determinism_cost.py select pick and check the pictures wai_determinism_cost.py export [q ...] pinned checkpoints -> integer sets wai_determinism_cost.py encode [q ...] both codecs; the float decode on this CPU wai_determinism_cost.py jobs the integer decode's job list wai_determinism_cost.py fdecode [--device cpu|mps|cuda] [--threads N] decode every float stream again wai_determinism_cost.py fserve [--device D] [--threads N] [--ids a,b] the timed float child of wai_det_energy.py wai_determinism_cost.py metrics distortion of every decode wai_determinism_cost.py bd BD-rate, integer against float wai_determinism_cost.py record the record: raw readings and their summary Needs the pinned stack of tools/requirements-sota-int.txt for `export`, `encode` and `fdecode`; `select`, `jobs`, `metrics` and `bd` need numpy and Pillow only. """ import hashlib import importlib.util import io import json import math import os import struct import sys import numpy as np HERE = os.path.dirname(os.path.abspath(__file__)) QUALITIES = list(range(1, 9)) # The published mbt2018_mean (MSE) checkpoints, by quality: the file the library's zoo # names, and the SHA-256 of its bytes, measured when this record was made. A checkpoint # with other bytes is refused before it is loaded. Quality 6 is the one # wai_sota_int_synth_export.py pins, and its digest is the same. CKPT_SHA256 = { 1: "e522738dc4599d1538fcdb16e21aa56df9146d60e8e083542466b387c1960156", 2: "e54a039d587afddd086b836cdd8eed4e0343d7cdd91028489ad0f3e7a72d307d", 3: "723404a8a039371abe8a14f05160ee1614cf5e0a3fb2a4e5f7ba569b68c22601", 4: "6dba02a30c78ff78baafd2072dd78646c27af6c4dc8416097c2c2858c1f186ce", 5: "d504e8ebe7e6396efd0b5fd093a32136714fd7e4be03db389e6f07aee24fdb39", 6: "a19628aba19732125e02caf5c38e233edbe2a641145a5b1779c51f4527f51dd6", 7: "d5d441d16ba3e5a80ead947e35d2dddc12fb0d9785a8006e9b35a0cef7668d91", 8: "8089ae3e23d9df5b7c68107c82c31da21019147aedddb03f03350de58d5385b7", } # The pictures: the reference manifest's PNG entries that it marks `self_test`, each one # picture of 768x512 or 512x768 (wai/tools/wai_visual_provenance_check.py's manifest). MANIFEST = "wai-image-references.json" MANIFEST_SHA256 = "db33c6633f89a66d6742a37cd348208792f6d49810da739ddf26d10dc6554b66" def sha256(b): return hashlib.sha256(b).hexdigest() def jdump(obj, path): with open(path, "w") as f: json.dump(obj, f, indent=1, sort_keys=True) f.write("\n") def jload(path): with open(path) as f: return json.load(f) def export_module(): spec = importlib.util.spec_from_file_location("sota_export", os.path.join(HERE, "wai_sota_int_synth_export.py")) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) return m # ---------------------------------------------------------------- select def cmd_select(work, root): raw = open(os.path.join(root, MANIFEST), "rb").read() if sha256(raw) != MANIFEST_SHA256: sys.exit(f"the manifest's SHA-256 is {sha256(raw)}, not the pinned {MANIFEST_SHA256}") man = json.loads(raw) from PIL import Image pics = [] for e in man["entries"]: if not (e.get("self_test") and e["path"].endswith(".png") and len(e["images"]) == 1): continue im = e["images"][0] if (im["width"], im["height"]) not in ((768, 512), (512, 768)): continue b = open(os.path.join(root, e["path"]), "rb").read() if sha256(b) != e["sha256"]: sys.exit(f"an entry's bytes do not match the manifest ({e['sha256'][:16]})") px = np.array(Image.open(io.BytesIO(b)).convert("RGB")) h, w = px.shape[:2] assert h % 64 == 0 and w % 64 == 0 pid = e["sha256"][:16] os.makedirs(os.path.join(work, "src"), exist_ok=True) px.tofile(os.path.join(work, "src", f"{pid}.rgb")) pics.append({"id": pid, "file_sha256": e["sha256"], "width": int(w), "height": int(h), "rgb_sha256": sha256(px.tobytes())}) jdump({"manifest_sha256": MANIFEST_SHA256, "rule": "PNG entries marked self_test with one picture of 768x512 or 512x768", "pictures": pics}, os.path.join(work, "pictures.json")) print(f"{len(pics)} pictures") def pictures(work): return jload(os.path.join(work, "pictures.json"))["pictures"] def source_rgb(work, p): return np.fromfile(os.path.join(work, "src", f"{p['id']}.rgb"), dtype=np.uint8).reshape(p["height"], p["width"], 3) # ---------------------------------------------------------------- models def load_model(q, allow_new=False): """mbt2018_mean (MSE) at quality q from the published checkpoint, its bytes checked against CKPT_SHA256 before they are loaded, as tensors only.""" import torch from compressai.zoo import image as zoo_image from compressai.zoo.pretrained import load_pretrained url = zoo_image.model_urls["mbt2018-mean"]["mse"][q] path = os.path.join(torch.hub.get_dir(), "checkpoints", os.path.basename(url)) if not os.path.exists(path): os.makedirs(os.path.dirname(path), exist_ok=True) torch.hub.download_url_to_file(url, path, progress=False) blob = open(path, "rb").read() got = sha256(blob) # The zoo's file names carry the first eight hex digits of the digest. if not os.path.basename(url).split("-")[-1].startswith(got[:8]): sys.exit(f"q{q}: {path} has SHA-256 {got}, which does not match the prefix in its name") want = CKPT_SHA256[q] if want is None and not allow_new: sys.exit(f"q{q}: no pinned digest; measured {got}") if want is not None and got != want: sys.exit(f"q{q}: {path} has SHA-256 {got}, pinned {want}") state = torch.load(io.BytesIO(blob), map_location="cpu", weights_only=True) m = zoo_image.model_architectures["mbt2018-mean"].from_state_dict(load_pretrained(state)).eval() return m, {"quality": q, "url_name": os.path.basename(url), "sha256": got} # ---------------------------------------------------------------- integer export def int_net(E, seq, kind): """One integer network from a float Sequential, quantised as E (the quality-6 export tool) quantises g_s and h_s.""" net = {"afrac": E.AFRAC, "gf": E.GF, "shift": E.SHIFT, "layers": []} import torch for layer in seq.children(): t = type(layer).__name__ if t == "ConvTranspose2d": wi, sc = E.q_weight_pc(layer.weight.detach().numpy()) net["layers"].append({"type": "convT", "in_ch": int(wi.shape[0]), "out_ch": int(wi.shape[1]), "k": int(wi.shape[2]), "stride": int(layer.stride[0]), "pad": int(layer.padding[0]), "opad": int(layer.output_padding[0]), "weight": wi.reshape(-1).tolist(), "mult": np.round(sc * (2 ** E.SHIFT)).astype(np.int64).tolist(), "bias": np.round(layer.bias.detach().numpy() * (2 ** E.AFRAC)).astype(np.int64).tolist()}) elif t == "Conv2d": wi, sc = E.q_weight_conv(layer.weight.detach().numpy()) net["layers"].append({"type": "conv", "in_ch": int(wi.shape[1]), "out_ch": int(wi.shape[0]), "k": int(wi.shape[2]), "stride": int(layer.stride[0]), "pad": int(layer.padding[0]), "weight": wi.reshape(-1).tolist(), "mult": np.round(sc * (2 ** E.SHIFT)).astype(np.int64).tolist(), "bias": np.round(layer.bias.detach().numpy() * (2 ** E.AFRAC)).astype(np.int64).tolist()}) elif t == "GDN": with torch.no_grad(): beta = layer.beta_reparam(layer.beta).numpy() C = beta.shape[0] gamma = layer.gamma_reparam(layer.gamma).reshape(C, C).numpy() net["layers"].append({"type": "igdn", "c": int(C), "gamma": np.round(gamma * (2 ** E.GF)).astype(np.int64).reshape(-1).tolist(), "beta": np.round(beta * (2 ** (2 * E.AFRAC))).astype(np.int64).tolist()}) elif t == "ReLU": net["layers"].append({"type": "relu"}) elif t == "LeakyReLU": net["layers"].append({"type": "leaky", "slope_q": 655, "slope_frac": 16}) else: sys.exit(f"{kind}: no integer form for a {t} layer") return net def requant(raw, mult, bias, shift): """((raw * mult) >> shift) + bias per output channel, exact: in int64 where the product provably fits, else in Python integers.""" if np.abs(raw).max(initial=0) * np.abs(mult).max(initial=0) < 2 ** 62: return ((raw * mult[:, None, None]) >> shift) + bias[:, None, None] return ((raw.astype(object) * mult[:, None, None]) >> shift).astype(np.int64) + bias[:, None, None] def conv_t_fast(x, L, shift): """The integer transposed convolution of `int_hyper_synth` (weight [in,out,k,k]), tap by tap: out[:, iy*s+ky, ix*s+kx] += w[:, :, ky, kx]^T x[:, iy, ix]. Integer sums are exact in any order, so this equals the export tool's per-pixel scatter.""" cin, co, k, s, p, op = L["in_ch"], L["out_ch"], L["k"], L["stride"], L["pad"], L["opad"] w = np.array(L["weight"], dtype=np.int64).reshape(cin, co, k, k) _, H, W = x.shape Ho, Wo = (H - 1) * s + k + op - 2 * p, (W - 1) * s + k + op - 2 * p raw = np.zeros((co, Ho + 2 * p, Wo + 2 * p), dtype=np.int64) xf = x.reshape(cin, -1) assert np.abs(x).max(initial=0) * np.abs(w).max(initial=0) * cin * k * k < 2 ** 63 for ky in range(k): for kx in range(k): c = (w[:, :, ky, kx].T @ xf).reshape(co, H, W) raw[:, ky:ky + s * H:s, kx:kx + s * W:s] += c raw = raw[:, p:p + Ho, p:p + Wo] return requant(raw, np.array(L["mult"], dtype=np.int64), np.array(L["bias"], dtype=np.int64), shift) def conv_fast(x, L, shift): cin, co, k, s, p = L["in_ch"], L["out_ch"], L["k"], L["stride"], L["pad"] w = np.array(L["weight"], dtype=np.int64).reshape(co, cin, k, k) _, H, W = x.shape Ho, Wo = (H + 2 * p - k) // s + 1, (W + 2 * p - k) // s + 1 xp = np.zeros((cin, H + 2 * p, W + 2 * p), dtype=np.int64) xp[:, p:p + H, p:p + W] = x assert np.abs(x).max(initial=0) * np.abs(w).max(initial=0) * cin * k * k < 2 ** 63 raw = np.zeros((co, Ho, Wo), dtype=np.int64) for ky in range(k): for kx in range(k): sub = xp[:, ky:ky + s * Ho:s, kx:kx + s * Wo:s].reshape(cin, -1) raw += (w[:, :, ky, kx] @ sub).reshape(co, Ho, Wo) return requant(raw, np.array(L["mult"], dtype=np.int64), np.array(L["bias"], dtype=np.int64), shift) def run_hs(net, x): """The integer hyper-decoder (convolutions and ReLUs only), as `int_hyper_synth` runs it.""" for L in net["layers"]: t = L["type"] if t == "convT": x = conv_t_fast(x, L, net["shift"]) elif t == "conv": x = conv_fast(x, L, net["shift"]) elif t == "relu": x = np.maximum(x, 0) elif t == "leaky": x = np.where(x >= 0, x, (x * L["slope_q"]) >> L["slope_frac"]) else: sys.exit(f"h_s has a {t} layer") return x def cmd_export(work, qs, allow_new=False): """Per quality, the integer set (integer-payloads §9: entropy.json, g_s.whm, h_s.whm) and the model's identity.""" import torch from compressai.entropy_models import GaussianConditional torch.use_deterministic_algorithms(True) torch.set_num_threads(1) E = export_module() out = {} for q in qs: m, ident = load_model(q, allow_new) g_s = int_net(E, m.g_s, "g_s") h_s = int_net(E, m.h_s, "h_s") eb = m.entropy_bottleneck eb.update() median = eb._get_medians().reshape(-1).detach().numpy().astype(np.float64) scale_table = list(torch.exp(torch.linspace(np.log(0.11), np.log(256.0), 64)).numpy().astype(float)) gc = GaussianConditional(scale_table) gc.update() ent = {"afrac": E.AFRAC, "frac": E.AFRAC, "scale_edges": np.floor(np.array(scale_table) * (2 ** E.AFRAC)).astype(np.int64).tolist(), "cdfs": gc._quantized_cdf.tolist(), "cdf_lengths": gc._cdf_length.reshape(-1).int().tolist(), "offsets": gc._offset.reshape(-1).int().tolist(), "z_cdfs": eb._quantized_cdf.tolist(), "z_cdf_lengths": eb._cdf_length.reshape(-1).int().tolist(), "z_offsets": eb._offset.reshape(-1).int().tolist(), "z_median_q": np.round(median * (2 ** E.AFRAC)).astype(np.int64).tolist()} d = os.path.join(work, "sets", f"q{q}") os.makedirs(d, exist_ok=True) open(os.path.join(d, "g_s.whm"), "wb").write(E.model_to_bin(g_s)) open(os.path.join(d, "h_s.whm"), "wb").write(E.model_to_bin(h_s)) with open(os.path.join(d, "entropy.json"), "w") as f: json.dump(ent, f) jdump(h_s, os.path.join(d, "h_s.json")) # the encoder's copy of h_s ident["set"] = {n: sha256(open(os.path.join(d, n), "rb").read()) for n in ("entropy.json", "g_s.whm", "h_s.whm")} ident["N"], ident["M"] = int(m.N), int(m.M) out[q] = ident print(f"q{q}: N={m.N} M={m.M} checkpoint {ident['sha256'][:16]}… set g_s {ident['set']['g_s.whm'][:16]}…") path = os.path.join(work, "models.json") prev = jload(path) if os.path.exists(path) else {} prev.update({str(k): v for k, v in out.items()}) jdump(prev, path) # ---------------------------------------------------------------- encode def to_rgb8(x_hat): """A float reconstruction in [0, 1] as 8-bit samples: x·255 rounded half to even, as the float library's own tensor arithmetic gives it on the device it ran on.""" import torch return (x_hat.clamp(0, 1) * 255).round().to(torch.uint8)[0].permute(1, 2, 0).contiguous().cpu().numpy() def float_container(strings, zshape): y, z = strings[0][0], strings[1][0] return b"DETF" + struct.pack("