minijev-snake / code /minijev /export /onnx_export.py
markuz89's picture
MiniJEV v2 model export
026f63e verified
Raw History Blame Contribute Delete
4.9 kB
"""PyTorch checkpoint -> ONNX (+ parity check, + optional INT8 dynamic quantization).
The exported graph takes (state, question_ids, choice_ids) with dynamic batch,
number of choices and state size, and returns (probs, logits, embedding); the
calibration temperature is baked in. Everything the runtime needs to
pre-process inputs is stored in the model's metadata (key "minijev").
"""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import torch
from minijev.inference.runtime import META_KEY, OnnxDecider, TorchDecider
from minijev.model import STATE_INPUTS, ExportWrapper
def export_onnx(checkpoint: str | Path, out: str | Path | None = None, opset: int = 18,
verify: bool = True, quantize: bool = True) -> dict:
import onnx
checkpoint = Path(checkpoint)
out = Path(out) if out else checkpoint.with_suffix(".onnx")
torch_dec = TorchDecider(checkpoint)
model, cfg, task = torch_dec.model, torch_dec.cfg, torch_dec.task
state_key = STATE_INPUTS[task.state_encoder]
wrapper = ExportWrapper(model, state_key).eval()
states = task.example_states(4)
question = task.default_question
choices = list(task.default_choices)
x = torch_dec.encode(states)
q, c = torch_dec._tokens(question, tuple(choices))
B = len(states)
args = (torch.as_tensor(x), torch.as_tensor(np.repeat(q, B, 0)), torch.as_tensor(np.repeat(c, B, 0)))
state_axes = {0: "batch", 2: "height", 3: "width"} if x.ndim == 4 else {0: "batch", 1: "length"}
dynamic_axes = {state_key: state_axes, "question_ids": {0: "batch"},
"choice_ids": {0: "batch", 1: "n_choices"},
"probs": {0: "batch", 1: "n_choices"}, "logits": {0: "batch", 1: "n_choices"},
"embedding": {0: "batch"}}
# TorchScript-based exporter: mature, handles SDPA with masks and dynamic axes well
torch.onnx.export(wrapper, args, str(out), input_names=[state_key, "question_ids", "choice_ids"],
output_names=["probs", "logits", "embedding"], dynamic_axes=dynamic_axes,
opset_version=opset, dynamo=False)
meta = {"task": task.name, "task_cfg": cfg["task"], "state_input": state_key,
"max_question_len": torch_dec.max_question_len, "max_choice_len": torch_dec.max_choice_len,
"temperature": model.temperature, "params": model.num_parameters(), "config_name": cfg.get("name"),
"default_question": question, "default_choices": choices}
m = onnx.load(str(out))
entry = m.metadata_props.add()
entry.key, entry.value = META_KEY, json.dumps(meta)
onnx.save(m, str(out))
report = {"onnx": str(out), "size_mb": round(out.stat().st_size / 2**20, 3), "meta": meta}
if verify:
report["parity_fp32"] = parity(torch_dec, OnnxDecider(out), task)
if quantize:
q_out = out.with_name(out.stem + ".int8.onnx")
quantize_int8(out, q_out)
report["onnx_int8"] = str(q_out)
report["size_int8_mb"] = round(q_out.stat().st_size / 2**20, 3)
if verify:
report["parity_int8"] = parity(torch_dec, OnnxDecider(q_out), task)
return report
def parity(ref, other, task, n: int = 256, seed: int = 1) -> dict:
"""Compare probabilities on real states, including 2- and 3-choice subsets."""
states = task.example_states(n, seed=seed)
choices = list(task.default_choices)
res = {}
for name, ch in (("all", choices), ("subset", choices[: max(2, len(choices) - 1)])):
p_ref = ref.probs(states, task.default_question, ch)
p_oth = other.probs(states, task.default_question, ch)
res[name] = {"max_abs_diff": float(np.abs(p_ref - p_oth).max()),
"argmax_agreement": float((p_ref.argmax(1) == p_oth.argmax(1)).mean())}
return res
def quantize_int8(src: str | Path, dst: str | Path) -> None:
"""Dynamic INT8 quantization of MatMul/Gemm weights (activations stay float).
Conv layers are left in fp32: dynamic ConvInteger kernels are slow on most
CPUs and the stem is a small share of the compute.
"""
from onnxruntime.quantization import QuantType, quantize_dynamic
from onnxruntime.quantization.shape_inference import quant_pre_process
pre = Path(str(dst) + ".pre.onnx")
try:
quant_pre_process(str(src), str(pre), skip_symbolic_shape=True)
source = pre
except Exception:
source = Path(src)
quantize_dynamic(str(source), str(dst), weight_type=QuantType.QInt8, op_types_to_quantize=["MatMul", "Gemm"])
if pre.exists():
pre.unlink()
import onnx
m = onnx.load(str(dst))
src_meta = {p.key: p.value for p in onnx.load(str(src)).metadata_props}
del m.metadata_props[:]
for k, v in src_meta.items():
e = m.metadata_props.add()
e.key, e.value = k, v
onnx.save(m, str(dst))