Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 7s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m41s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 2m17s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 3m24s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 4m35s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m9s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 5m26s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 6m55s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 7m42s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · does this change touch it (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-audit collector and player integration / does this change touch it (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
WAI operation catalogue / does this change touch it (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-fleet service / does this change touch it (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-quantum for JavaScript / does this change touch it (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / does this change touch it (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / does this change touch it (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / does this change touch it (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / does this change touch it (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-key-proxy / does this change touch it (push) Has been cancelled
wai-key-proxy / proxy · linux (push) Has been cancelled
extensions/neural-dispatch.md: which unit runs a neural capability's networks (neural processor, GPU, CPU), the tier a decode needs, no float path for a decode that needs decode-equivalence, the choice and the report wai-neural-dispatch/1 with the backend's placement. wai-rs: neural_dispatch (pure) and the neural_npu feature, bmshj2018's synthesis through the platform's model runtime on macOS, placed by its compute plan. wai-web: the neural_npu bundle and a WebNN page. tools/wai_bmshj2018_npu_set.py writes the set. Co-Authored-By: Claude Opus 5.5 <[email protected]>
221 lines
9.3 KiB
Python
221 lines
9.3 KiB
Python
"""Write a `wai.neural.bmshj2018` parameter set that a sink can decode on its CPU, on
|
|
the platform's neural processor through the platform's model runtime, and in a browser
|
|
through WebNN (extensions/neural-dispatch.md §6), and one payload to decode.
|
|
|
|
The set, in one directory (`--out`):
|
|
|
|
decoder.onnx the synthesis transform g_s, float32, for the CPU path
|
|
(the same graph `wai_bmshj2018_export_onnx.py` writes)
|
|
cdfs_q<N>.json the entropy bottleneck's tables (`wai_bmshj2018_extract_cdfs.py`)
|
|
decoder.mlmodel g_s as an ML Program for the platform's model runtime, computed
|
|
decoder.weight.bin in float16, its input fixed at [1, C, S, S]: the program's
|
|
specification and its weight file, which a sink places in a
|
|
package of its own (neural-dispatch §6.1)
|
|
decoder.webnn.json g_s as a list of layers over one float32 file, for a browser's
|
|
decoder.f32 WebNN graph builder (neural-dispatch §6.2)
|
|
wai-usage.json the set's usage statement (prior-carriage §18.1): the licence
|
|
the weights are published under
|
|
|
|
and beside it the payload `vector.bmshj2018` (a 256 x 256 picture this tool draws from a
|
|
seed, coded with the model's own encoder) and `vector.json`, what it was made from.
|
|
|
|
The weights are CompressAI's pretrained bmshj2018-factorized model, published with the
|
|
library under its licence, BSD-3-Clause-Clear; the statement records it. GDN's
|
|
reparameterised beta and gamma are taken as values once, so the converters see plain
|
|
convolutions, multiplications and square roots.
|
|
|
|
Usage (needs torch, compressai, coremltools, onnx; the model runtime's conversion runs on
|
|
any host, its compile and the compute plan only on the platform):
|
|
|
|
python3 tools/wai_bmshj2018_npu_set.py --out <dir> [--quality 3] [--size 256]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
import struct
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import torch
|
|
from compressai.layers import GDN
|
|
from compressai.zoo import models
|
|
|
|
LICENCE = "BSD-3-Clause-Clear"
|
|
|
|
|
|
class PlainGDN(torch.nn.Module):
|
|
"""GDN / IGDN with its reparameterised parameters as constants."""
|
|
|
|
def __init__(self, g: GDN):
|
|
super().__init__()
|
|
c = g.beta.shape[0]
|
|
self.register_buffer("beta", g.beta_reparam(g.beta).detach().clone())
|
|
self.register_buffer("gamma", g.gamma_reparam(g.gamma).detach().clone().reshape(c, c, 1, 1))
|
|
self.inverse = g.inverse
|
|
|
|
def forward(self, x):
|
|
n = torch.sqrt(torch.nn.functional.conv2d(x * x, self.gamma, self.beta))
|
|
return x * n if self.inverse else x / n
|
|
|
|
|
|
class Synthesis(torch.nn.Module):
|
|
"""g_s, clamped to [0, 1]: the contract of `wai.neural.bmshj2018`'s decoder."""
|
|
|
|
def __init__(self, g_s):
|
|
super().__init__()
|
|
self.g_s = g_s
|
|
|
|
def forward(self, latents):
|
|
return self.g_s(latents).clamp(0.0, 1.0)
|
|
|
|
|
|
def plain(g_s) -> torch.nn.Sequential:
|
|
return torch.nn.Sequential(*[PlainGDN(m) if isinstance(m, GDN) else m for m in g_s])
|
|
|
|
|
|
def picture(size: int, seed: int) -> np.ndarray:
|
|
"""A picture drawn from `seed`: two gradients and twelve discs, RGB in [0, 1]."""
|
|
rng = np.random.default_rng(seed)
|
|
yy, xx = np.mgrid[0:size, 0:size].astype(np.float32) / (size - 1)
|
|
img = np.stack([xx, yy, 0.5 + 0.5 * np.sin(6.0 * (xx + yy))], axis=-1) * 0.6 + 0.2
|
|
for _ in range(12):
|
|
cx, cy, r = rng.uniform(0, 1), rng.uniform(0, 1), rng.uniform(0.04, 0.2)
|
|
col = rng.uniform(0, 1, 3).astype(np.float32)
|
|
img[(xx - cx) ** 2 + (yy - cy) ** 2 < r * r] = col
|
|
return np.clip(img, 0.0, 1.0)
|
|
|
|
|
|
def webnn_layers(seq: torch.nn.Sequential):
|
|
"""The layer list and the float32 values decoder.webnn.json and decoder.f32 hold."""
|
|
values: list[np.ndarray] = []
|
|
offset = 0
|
|
|
|
def put(t: torch.Tensor):
|
|
nonlocal offset
|
|
a = t.detach().cpu().float().numpy().astype("<f4")
|
|
values.append(a.reshape(-1))
|
|
ref = {"offset": offset, "shape": list(a.shape)}
|
|
offset += a.size
|
|
return ref
|
|
|
|
layers = []
|
|
for m in seq:
|
|
if isinstance(m, torch.nn.ConvTranspose2d):
|
|
p, op = m.padding, m.output_padding
|
|
layers.append({
|
|
"op": "conv_transpose2d", "weight": put(m.weight), "bias": put(m.bias),
|
|
"strides": list(m.stride), "padding": [p[0], p[0], p[1], p[1]],
|
|
"output_padding": list(op),
|
|
})
|
|
elif isinstance(m, PlainGDN):
|
|
layers.append({"op": "igdn" if m.inverse else "gdn", "gamma": put(m.gamma), "beta": put(m.beta)})
|
|
else:
|
|
raise SystemExit(f"g_s holds a layer this form does not write: {m!r}")
|
|
return layers, np.concatenate(values)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--out", required=True)
|
|
ap.add_argument("--quality", type=int, default=3)
|
|
ap.add_argument("--size", type=int, default=256, help="the picture's side, a multiple of 16")
|
|
ap.add_argument("--seed", type=int, default=20261109)
|
|
args = ap.parse_args()
|
|
if args.size % 16:
|
|
raise SystemExit("--size must be a multiple of 16")
|
|
out = Path(args.out)
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
|
|
net = models["bmshj2018-factorized"](quality=args.quality, pretrained=True).eval()
|
|
net.update(force=True)
|
|
seq = plain(net.g_s)
|
|
syn_plain = Synthesis(seq).eval()
|
|
syn_onnx = Synthesis(net.g_s).eval()
|
|
|
|
# The payload: the picture coded by the model's own encoder.
|
|
img = picture(args.size, args.seed)
|
|
x = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0)
|
|
with torch.no_grad():
|
|
coded = net.compress(x)
|
|
y_hat = net.entropy_bottleneck.decompress(coded["strings"][0], coded["shape"])
|
|
rans = coded["strings"][0][0]
|
|
s = coded["shape"][0]
|
|
c = int(net.entropy_bottleneck._quantized_cdf.shape[0])
|
|
s_log2 = int(s).bit_length() - 1
|
|
assert 1 << s_log2 == s and coded["shape"][1] == s
|
|
payload = struct.pack("<IIIIBB", args.size, args.size, len(rans), args.quality, c, s_log2) + rans
|
|
(out / "vector.bmshj2018").write_bytes(payload)
|
|
|
|
# The entropy tables (wai_bmshj2018_extract_cdfs.py's form).
|
|
eb = net.entropy_bottleneck
|
|
cdf_len = eb._cdf_length.cpu().int().tolist()
|
|
cdf = eb._quantized_cdf.cpu().int().tolist()
|
|
blob = {
|
|
"model": "bmshj2018-factorized", "quality": args.quality, "precision": 16, "bypass_precision": 4,
|
|
"channels": len(cdf_len), "cdf_length": cdf_len, "offset": eb._offset.cpu().int().tolist(),
|
|
"medians": eb._get_medians().detach().reshape(-1).cpu().float().tolist(),
|
|
"cdf": [row[:cdf_len[i]] for i, row in enumerate(cdf)],
|
|
}
|
|
(out / f"cdfs_q{args.quality}.json").write_text(json.dumps(blob, separators=(",", ":")))
|
|
|
|
with torch.no_grad():
|
|
ref = syn_onnx(y_hat)
|
|
diff = float((syn_plain(y_hat) - ref).abs().max())
|
|
if diff != 0.0:
|
|
raise SystemExit(f"the plain g_s differs from CompressAI's by {diff}")
|
|
|
|
# The CPU path's ONNX graph, as wai_bmshj2018_export_onnx.py writes it.
|
|
torch.onnx.export(
|
|
syn_onnx, (y_hat,), (out / "decoder.onnx").as_posix(),
|
|
input_names=["latents"], output_names=["image"],
|
|
dynamic_axes={"latents": {2: "S", 3: "S"}, "image": {2: "H", 3: "W"}},
|
|
opset_version=17, do_constant_folding=True, dynamo=False,
|
|
)
|
|
|
|
# The ML Program, float16, input fixed at the payload's latent shape.
|
|
import coremltools as ct
|
|
|
|
traced = torch.jit.trace(syn_plain, y_hat)
|
|
prog = ct.convert(
|
|
traced, inputs=[ct.TensorType(name="latents", shape=tuple(y_hat.shape))],
|
|
outputs=[ct.TensorType(name="image")], convert_to="mlprogram",
|
|
compute_precision=ct.precision.FLOAT16, minimum_deployment_target=ct.target.macOS14,
|
|
)
|
|
with tempfile.TemporaryDirectory() as t:
|
|
pkg = Path(t) / "g_s.mlpackage"
|
|
prog.save(pkg.as_posix())
|
|
data = pkg / "Data" / "com.apple.CoreML" # allow-name: the package format's fixed directory name
|
|
shutil.copyfile(data / "model.mlmodel", out / "decoder.mlmodel")
|
|
shutil.copyfile(data / "weights" / "weight.bin", out / "decoder.weight.bin")
|
|
|
|
# The WebNN form.
|
|
layers, values = webnn_layers(seq)
|
|
(out / "decoder.f32").write_bytes(values.astype("<f4").tobytes())
|
|
(out / "decoder.webnn.json").write_text(json.dumps({
|
|
"format": "wai-bmshj2018-webnn/1",
|
|
"input": {"name": "latents", "shape": list(y_hat.shape)},
|
|
"output": {"name": "image", "shape": [1, 3, args.size, args.size]},
|
|
"clamp": [0.0, 1.0],
|
|
"layers": layers,
|
|
}, indent=1) + "\n")
|
|
|
|
(out / "wai-usage.json").write_text(json.dumps({
|
|
"kind": "wai-set-usage", "licence": LICENCE, "terms": None,
|
|
"content_derived": False, "redistribute": True, "ai_usage": None,
|
|
}) + "\n")
|
|
(out / "vector.json").write_text(json.dumps({
|
|
"capability": "wai.neural.bmshj2018", "quality": args.quality, "size": args.size,
|
|
"seed": args.seed, "latent": [1, c, s, s], "payload_bytes": len(payload),
|
|
"picture": "tools/wai_bmshj2018_npu_set.py picture(size, seed)",
|
|
"weights": "CompressAI bmshj2018-factorized, pretrained", "licence": LICENCE,
|
|
}, indent=1) + "\n")
|
|
for f in sorted(out.iterdir()):
|
|
print(f"{f.name:22} {f.stat().st_size:>10,} B")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|