wai/tools: wai_jpegai_export_weights.py — reproducible weight export for the JPEG-AI decoder #27
1 changed files with 226 additions and 0 deletions
226
wai/tools/wai_jpegai_export_weights.py
Normal file
226
wai/tools/wai_jpegai_export_weights.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
"""Export JPEG-AI decoder weights from the ISO reference ONNX assets.
|
||||
|
||||
The byte-exact decoder in `wai-rs` (`jpegai_synthesis`, `jpegai_pipeline`,
|
||||
`jpegai_icci`) is driven by weights lifted straight out of the reference
|
||||
`jpegai-vm-assets` ONNX files. Two regimes, and using the wrong one silently
|
||||
produces plausible-but-wrong output:
|
||||
|
||||
float nets — synthesis (model_y / model_uv), hyper_decoder, MCM stages.
|
||||
Exported as 16-bit per-tensor symmetric PTQ:
|
||||
conv weight [O, I/g, kh, kw] -> int16 + per-tensor
|
||||
mult = round(scale * 2^31), shift = 31
|
||||
bias / LayerNorm / temperature -> Q16 (round(v * 2^16))
|
||||
This is what `jpegai_synthesis::Weights::from_json` expects.
|
||||
|
||||
integer net — hyper_scale_decoder. Already integer end to end (int32 conv
|
||||
weights, integer-valued Pow exponents); exported VERBATIM with
|
||||
no quantization, for `jpegai_synthesis::IntNet::from_json`.
|
||||
Quantizing it would corrupt an exactly-representable net.
|
||||
|
||||
Reading the file is dependency-free: ONNX is protobuf, and we only need the
|
||||
initializer table, so there is no `onnx` / `protobuf` / `torch` requirement.
|
||||
|
||||
ModelProto.graph = 7
|
||||
GraphProto.initializer = 5
|
||||
TensorProto{ dims=1, data_type=2, name=8, raw_data=9 }
|
||||
data_type: 1 = float32, 6 = int32, 7 = int64
|
||||
|
||||
Usage
|
||||
-----
|
||||
# float net -> Q16 PTQ (synthesis / hyper_decoder / MCM stage)
|
||||
python wai_jpegai_export_weights.py ptq synthesis16.onnx synth16_y.json
|
||||
|
||||
# integer net -> verbatim
|
||||
python wai_jpegai_export_weights.py int hyper_scale_decoder.onnx hs_y.json
|
||||
|
||||
# just look at what is inside
|
||||
python wai_jpegai_export_weights.py list model_15.onnx
|
||||
|
||||
Caveat worth knowing: the eICCI post-filter models under `post_filters/` export
|
||||
the model's `forward`, which the reference decoder never calls (it calls
|
||||
`process_dwt444_2` + `process_comp`), and their initializers are BatchNorm-folded
|
||||
and incomplete — no `BNY`, no `scaleU`/`scaleV`, no per-residual-block `scale`.
|
||||
For eICCI take the reference's PyTorch `state_dict` instead; `list` is still
|
||||
useful there for inspection.
|
||||
"""
|
||||
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
SHIFT = 31 # conv requant shift, matches int_transformer::conv_grouped
|
||||
Q16 = 1 << 16 # bias / LN / temperature fixed-point scale
|
||||
INT16_MAX = 32767
|
||||
|
||||
|
||||
# ---- dependency-free protobuf reader ----
|
||||
|
||||
def _varint(buf, i):
|
||||
shift = value = 0
|
||||
while True:
|
||||
byte = buf[i]
|
||||
i += 1
|
||||
value |= (byte & 0x7F) << shift
|
||||
if not byte & 0x80:
|
||||
return value, i
|
||||
shift += 7
|
||||
|
||||
|
||||
def _fields(buf):
|
||||
"""Yield (field_number, wire_type, payload) for one protobuf message."""
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
tag, i = _varint(buf, i)
|
||||
field, wire = tag >> 3, tag & 7
|
||||
if wire == 0:
|
||||
value, i = _varint(buf, i)
|
||||
yield field, 0, value
|
||||
elif wire == 2:
|
||||
length, i = _varint(buf, i)
|
||||
yield field, 2, buf[i:i + length]
|
||||
i += length
|
||||
elif wire == 1:
|
||||
yield field, 1, buf[i:i + 8]
|
||||
i += 8
|
||||
elif wire == 5:
|
||||
yield field, 5, buf[i:i + 4]
|
||||
i += 4
|
||||
else:
|
||||
raise ValueError(f"unsupported wire type {wire}")
|
||||
|
||||
|
||||
def _parse_tensor(buf):
|
||||
dims, name, dtype, raw = [], "", 0, b""
|
||||
for field, wire, payload in _fields(buf):
|
||||
if field == 1:
|
||||
if wire == 0:
|
||||
dims.append(payload)
|
||||
elif wire == 2: # packed dims
|
||||
j = 0
|
||||
while j < len(payload):
|
||||
d, j = _varint(payload, j)
|
||||
dims.append(d)
|
||||
elif field == 2 and wire == 0:
|
||||
dtype = payload
|
||||
elif field == 8 and wire == 2:
|
||||
name = payload.decode("utf-8", "replace")
|
||||
elif field == 9 and wire == 2:
|
||||
raw = payload
|
||||
return name, dims, dtype, raw
|
||||
|
||||
|
||||
def initializers(path):
|
||||
"""name -> (dims, data_type, raw_bytes) for every initializer, in file order."""
|
||||
out = OrderedDict()
|
||||
model = open(path, "rb").read()
|
||||
for field, wire, payload in _fields(model):
|
||||
if field == 7 and wire == 2: # ModelProto.graph
|
||||
for gfield, gwire, gpayload in _fields(payload):
|
||||
if gfield == 5 and gwire == 2: # GraphProto.initializer
|
||||
name, dims, dtype, raw = _parse_tensor(gpayload)
|
||||
out[name] = (dims, dtype, raw)
|
||||
return out
|
||||
|
||||
|
||||
def _prod(xs):
|
||||
n = 1
|
||||
for x in xs:
|
||||
n *= x
|
||||
return n
|
||||
|
||||
|
||||
def _decode(dims, dtype, raw):
|
||||
"""raw bytes -> list of numbers, or None if the tensor is empty/unsupported."""
|
||||
n = _prod(dims)
|
||||
if not raw:
|
||||
return None
|
||||
if dtype == 1 and len(raw) == 4 * n:
|
||||
return list(struct.unpack(f"<{n}f", raw))
|
||||
if dtype == 6 and len(raw) == 4 * n:
|
||||
return list(struct.unpack(f"<{n}i", raw))
|
||||
if dtype == 7 and len(raw) == 8 * n:
|
||||
return list(struct.unpack(f"<{n}q", raw))
|
||||
return None
|
||||
|
||||
|
||||
# ---- export modes ----
|
||||
|
||||
def export_ptq(src, dst, strip_prefix=True):
|
||||
"""Float net -> 16-bit symmetric PTQ, the layout `Weights::from_json` reads."""
|
||||
import re
|
||||
result = OrderedDict()
|
||||
n_conv = n_scalar = total = 0
|
||||
worst = 0.0
|
||||
for name, (dims, dtype, raw) in initializers(src).items():
|
||||
values = _decode(dims, dtype, raw)
|
||||
if values is None or dtype != 1:
|
||||
continue
|
||||
total += len(values)
|
||||
# the reference wraps modules as `hop_<x>.inst.<name>`; the decoder
|
||||
# addresses them by the bare module path
|
||||
key = re.sub(r"^hop_\w+\.inst\.", "", name) if strip_prefix else name
|
||||
if len(dims) == 4 and dims[0] > 1: # conv weight [O, I/g, kh, kw]
|
||||
amax = max((abs(v) for v in values), default=0.0) or 1e-12
|
||||
scale = amax / INT16_MAX
|
||||
quant = [max(-32768, min(INT16_MAX, round(v / scale))) for v in values]
|
||||
worst = max(worst, max(abs(v - round(v / scale) * scale) for v in values))
|
||||
result[key] = {"kind": "conv", "dims": dims, "shift": SHIFT,
|
||||
"mult": round(scale * (1 << SHIFT)), "w": quant}
|
||||
n_conv += 1
|
||||
else: # bias / LN gamma,beta / temperature
|
||||
result[key] = {"kind": "q16", "dims": dims,
|
||||
"v": [round(v * Q16) for v in values]}
|
||||
n_scalar += 1
|
||||
json.dump(result, open(dst, "w"))
|
||||
print(f"{n_conv} conv + {n_scalar} scalar tensors ({total:,} params) -> {dst}")
|
||||
print(f"conv round-trip worst |error| = {worst:.3e} "
|
||||
f"({'lossless at 16 bits' if worst < 1e-3 else 'CHECK'})")
|
||||
|
||||
|
||||
def export_int(src, dst):
|
||||
"""Integer net -> verbatim. Pow exponents are integer-valued floats."""
|
||||
result = OrderedDict()
|
||||
for name, (dims, dtype, raw) in initializers(src).items():
|
||||
values = _decode(dims, dtype, raw)
|
||||
if values is None:
|
||||
continue
|
||||
if dtype == 1: # integer-valued float (Pow exponent)
|
||||
values = [int(round(v)) for v in values]
|
||||
result[name] = {"dims": dims, "v": values}
|
||||
json.dump(result, open(dst, "w"))
|
||||
print(f"{len(result)} integer tensors -> {dst}")
|
||||
for name, entry in result.items():
|
||||
lo, hi = min(entry["v"]), max(entry["v"])
|
||||
print(f" {name:24s} dims={entry['dims']} range=[{lo}, {hi}]")
|
||||
|
||||
|
||||
def list_tensors(src):
|
||||
dtypes = {1: "f32", 6: "i32", 7: "i64"}
|
||||
total = 0
|
||||
for name, (dims, dtype, raw) in initializers(src).items():
|
||||
n = _prod(dims)
|
||||
total += n
|
||||
print(f" {name:38s} {dtypes.get(dtype, dtype):4s} dims={dims}")
|
||||
print(f"{total:,} params")
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 3:
|
||||
print(__doc__)
|
||||
return 1
|
||||
mode, src = argv[1], argv[2]
|
||||
if mode == "list":
|
||||
list_tensors(src)
|
||||
elif mode == "ptq":
|
||||
export_ptq(src, argv[3])
|
||||
elif mode == "int":
|
||||
export_int(src, argv[3])
|
||||
else:
|
||||
print(f"unknown mode {mode!r}; expected list | ptq | int")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Loading…
Reference in a new issue