from __future__ import annotations from pathlib import Path import numpy as np import torch from model import BitMLP def pack_binary_model(model: BitMLP, destination: Path) -> dict: payload = {} payload_bytes = 0 for name in ["hidden", "output"]: layer = getattr(model, name) weight = layer.weight.detach().cpu().numpy() scales = np.mean(np.abs(weight), axis=1).astype(np.float32) signs = np.packbits((weight >= 0).reshape(-1), bitorder="little") shape = np.asarray(weight.shape, dtype=np.int32) bias = layer.bias.detach().cpu().numpy().astype(np.float32) payload[f"{name}_signs"] = signs payload[f"{name}_scales"] = scales payload[f"{name}_shape"] = shape payload[f"{name}_bias"] = bias payload_bytes += signs.nbytes + scales.nbytes + bias.nbytes np.savez(destination, **payload) return { "packed_payload_bytes": int(payload_bytes), "container_bytes": int(destination.stat().st_size), } def load_binary_model(source: Path) -> BitMLP: packed = np.load(source) model = BitMLP(bits="binary") with torch.no_grad(): for name in ["hidden", "output"]: layer = getattr(model, name) shape = tuple(packed[f"{name}_shape"].astype(int)) total = int(np.prod(shape)) bits = np.unpackbits( packed[f"{name}_signs"], bitorder="little" )[:total] signs = np.where(bits.reshape(shape) == 1, 1.0, -1.0) scales = packed[f"{name}_scales"][:, None] layer.weight.copy_( torch.from_numpy((signs * scales).astype(np.float32)) ) layer.bias.copy_(torch.from_numpy(packed[f"{name}_bias"])) return model