Jessedbrown's picture
Sync with GitHub main f966009 (part 4)
a1c23d8 verified
Raw History Blame Contribute Delete
9.89 kB
#!/usr/bin/env python3
"""Writes src/weights.rs: the vendored prototype_model.pt as Q24 integers, read WITHOUT torch.
The checkpoint is a torch zip; data.pkl is unpickled with a restricted Unpickler that resolves only
collections.OrderedDict, torch._utils._rebuild_tensor_v2 and torch.FloatStorage, and each tensor's bytes are
read from <archive>/data/<key> as little-endian float32. Each weight becomes floor(w * 2^24 + 1/2), exactly.
The state dict's keys and its _metadata module tree must be those of PrototypeGNN(6, 64, 2, 3) built with the
fallback GCNConv of prototype_gnn.py:17-24 (conv*.lin, no aggr_module); the Rust names follow those keys.
--check regenerates in memory and diffs.
"""
import collections
import hashlib
import io
import math
import pathlib
import pickle
import struct
import sys
import zipfile
from fractions import Fraction
ROOT = pathlib.Path(__file__).resolve().parent.parent
CHECKPOINT = ROOT / "vendor" / "fnns" / "models" / "prototype_model.pt"
CHECKPOINT_SHA256 = "b4df990138ae215bbdda7c1fcf4902bc0a8cb72d9b7d8944eb965d0fc737dd22"
WEIGHTS_RS = ROOT / "src" / "weights.rs"
FRAC_BITS = 24
VALUES_PER_LINE = 16
# (checkpoint key, Rust name, shape), in the checkpoint's order.
TENSORS = [
("prototypes", "PROTOTYPES", (2, 3, 64)),
("conv1.lin.weight", "CONV1_W", (64, 6)),
("conv1.lin.bias", "CONV1_B", (64,)),
("conv2.lin.weight", "CONV2_W", (64, 64)),
("conv2.lin.bias", "CONV2_B", (64,)),
("edge_projection.0.weight", "PROJ0_W", (64, 128)),
("edge_projection.0.bias", "PROJ0_B", (64,)),
("edge_projection.2.weight", "PROJ2_W", (64, 64)),
("edge_projection.2.bias", "PROJ2_B", (64,)),
]
# Module tree of PrototypeGNN with the fallback GCNConv (prototype_gnn.py:19-22 holds only self.lin).
MODULE_TREE = [
"",
"conv1",
"conv1.lin",
"conv2",
"conv2.lin",
"edge_projection",
"edge_projection.0",
"edge_projection.1",
"edge_projection.2",
]
class FloatStorageClass:
"""Stands in for the torch.FloatStorage global; never instantiated."""
class Storage:
def __init__(self, values):
self.values = values
class Tensor:
def __init__(self, shape, values):
self.shape = shape
self.values = values
def row_major_strides(shape):
strides = []
step = 1
for dim in reversed(shape):
strides.append(step)
step *= dim
return tuple(reversed(strides))
def rebuild_tensor_v2(storage, storage_offset, size, stride, requires_grad, backward_hooks, metadata=None):
# The subset of torch._utils._rebuild_tensor_v2 this checkpoint uses: a whole, contiguous float32 storage.
if not isinstance(storage, Storage):
raise pickle.UnpicklingError("tensor storage is not a float32 storage")
size, stride = tuple(size), tuple(stride)
if not all(isinstance(d, int) and d > 0 for d in size):
raise pickle.UnpicklingError(f"tensor size {size} is not positive integers")
if stride != row_major_strides(size):
raise pickle.UnpicklingError(f"tensor stride {stride} is not row-major for size {size}")
if storage_offset != 0 or math.prod(size) != len(storage.values):
raise pickle.UnpicklingError("tensor does not cover its whole storage")
if requires_grad is not False or backward_hooks or metadata is not None:
raise pickle.UnpicklingError("tensor carries grad, hooks or metadata")
return Tensor(size, storage.values)
class CheckpointUnpickler(pickle.Unpickler):
ALLOWED = {
("collections", "OrderedDict"): collections.OrderedDict,
("torch._utils", "_rebuild_tensor_v2"): rebuild_tensor_v2,
("torch", "FloatStorage"): FloatStorageClass,
}
def __init__(self, data, archive, prefix):
super().__init__(io.BytesIO(data))
self.archive = archive
self.prefix = prefix
def find_class(self, module, name):
try:
return self.ALLOWED[(module, name)]
except KeyError:
raise pickle.UnpicklingError(f"global {module}.{name} is not allowed") from None
def persistent_load(self, pid):
if not (isinstance(pid, tuple) and len(pid) == 5 and pid[0] == "storage"):
raise pickle.UnpicklingError(f"persistent id {pid!r} is not a storage")
_, storage_type, key, location, numel = pid
if storage_type is not FloatStorageClass or location != "cpu":
raise pickle.UnpicklingError(f"storage {key!r} is not a cpu FloatStorage")
if not (isinstance(key, str) and key.isdigit() and isinstance(numel, int) and numel > 0):
raise pickle.UnpicklingError(f"storage id {pid!r} is malformed")
raw = self.archive.read(f"{self.prefix}/data/{key}")
if len(raw) != 4 * numel:
raise pickle.UnpicklingError(f"storage {key} holds {len(raw)} bytes, expected {4 * numel}")
return Storage(struct.unpack(f"<{numel}f", raw))
def load_state_dict(path=CHECKPOINT):
blob = path.read_bytes()
digest = hashlib.sha256(blob).hexdigest()
if digest != CHECKPOINT_SHA256:
raise SystemExit(f"QUANTIZE|refused=sha256|got={digest}|want={CHECKPOINT_SHA256}")
with zipfile.ZipFile(io.BytesIO(blob)) as archive:
pickles = [n for n in archive.namelist() if n.endswith("/data.pkl")]
if len(pickles) != 1:
raise SystemExit("QUANTIZE|refused=archive|reason=expected one data.pkl")
prefix = pickles[0][: -len("/data.pkl")]
if archive.read(f"{prefix}/byteorder") != b"little":
raise SystemExit("QUANTIZE|refused=byteorder")
state = CheckpointUnpickler(archive.read(pickles[0]), archive, prefix).load()
if not isinstance(state, collections.OrderedDict):
raise SystemExit("QUANTIZE|refused=root|reason=not an OrderedDict")
if [key for key, _, _ in TENSORS] != list(state.keys()):
raise SystemExit(f"QUANTIZE|refused=keys|got={','.join(state.keys())}")
tree = list(getattr(state, "_metadata", {}).keys())
if tree != MODULE_TREE:
raise SystemExit(f"QUANTIZE|refused=module_tree|got={','.join(tree)}")
for key, _, shape in TENSORS:
if state[key].shape != shape:
raise SystemExit(f"QUANTIZE|refused=shape|key={key}|got={state[key].shape}")
return state
def q24(value):
return math.floor(Fraction(value) * (1 << FRAC_BITS) + Fraction(1, 2))
def q24_tensors(state=None):
"""{Rust name: (shape, row-major Q24 values)} of the pinned checkpoint."""
state = load_state_dict() if state is None else state
out = {}
for key, name, shape in TENSORS:
values = [q24(v) for v in state[key].values]
if not all(-(1 << 31) <= v < (1 << 31) for v in values):
raise SystemExit(f"QUANTIZE|refused=range|key={key}")
out[name] = (shape, values)
return out
def render_row(values, indent):
lines = []
for start in range(0, len(values), VALUES_PER_LINE):
lines.append(indent + ", ".join(str(v) for v in values[start:start + VALUES_PER_LINE]) + ",")
return lines
def render_matrix(values, rows, cols, indent):
out = []
for r in range(rows):
row = values[r * cols:(r + 1) * cols]
if cols <= VALUES_PER_LINE:
out.append(indent + "[" + ", ".join(str(v) for v in row) + "],")
else:
out.append(indent + "[")
out.extend(render_row(row, indent + " "))
out.append(indent + "],")
return out
def render(tensors):
out = [
f"//! Q24 weights, floor(w * 2^24 + 1/2), of vendor/fnns/models/prototype_model.pt (sha256 {CHECKPOINT_SHA256});",
"//! generated by tools/quantize.py, do not edit. Keys and module tree are PrototypeGNN(6, 64, 2, 3) with the",
"//! fallback GCNConv of prototype_gnn.py:17-24 (`conv*.lin.weight`, `conv*.lin.bias`).",
]
for key, name, shape in TENSORS:
_, values = tensors[name]
out.append("")
if len(shape) == 1:
out.append(f"/// `{key}` ({shape[0]}), Q24.")
out.append("#[rustfmt::skip]")
out.append(f"pub const {name}: [i32; {shape[0]}] = [")
out.extend(render_row(values, " "))
out.append("];")
elif len(shape) == 2:
rows, cols = shape
out.append(f"/// `{key}` ({rows} x {cols}, row = output), Q24.")
out.append("#[rustfmt::skip]")
out.append(f"pub const {name}: [[i32; {cols}]; {rows}] = [")
out.extend(render_matrix(values, rows, cols, " "))
out.append("];")
else:
classes, per_class, dim = shape
out.append(f"/// `{key}` ({classes} classes x {per_class} prototypes x {dim}), Q24.")
out.append("#[rustfmt::skip]")
out.append(f"pub const {name}: [[[i32; {dim}]; {per_class}]; {classes}] = [")
for c in range(classes):
out.append(" [")
block = values[c * per_class * dim:(c + 1) * per_class * dim]
out.extend(render_matrix(block, per_class, dim, " "))
out.append(" ],")
out.append("];")
return "\n".join(out) + "\n"
def main():
args = sys.argv[1:]
if args not in ([], ["--check"]):
print("usage: quantize.py [--check]", file=sys.stderr)
return 2
text = render(q24_tensors())
values = sum(math.prod(shape) for _, _, shape in TENSORS)
if args == ["--check"]:
if WEIGHTS_RS.read_text(encoding="utf-8") != text:
print(f"QUANTIZE|check=FAIL|file={WEIGHTS_RS.name}")
return 1
print(f"QUANTIZE|check=PASS|values={values}|sha256={CHECKPOINT_SHA256}")
return 0
WEIGHTS_RS.write_text(text, encoding="utf-8")
print(f"QUANTIZE|written={WEIGHTS_RS.name}|values={values}|sha256={CHECKPOINT_SHA256}")
return 0
if __name__ == "__main__":
sys.exit(main())