Download code/minijev/export/onnx_export.py from markuz89/minijev-snake: direct link, hf CLI and curl.
- Browser
- Download file 4.9 kB
-
https://huggingface.co/markuz89/minijev-snake/resolve/main/code/minijev/export/onnx_export.py
- Command line
-
hf download hf://markuz89/minijev-snake/code/minijev/export/onnx_export.py
-
curl -L -o onnx_export.py https://huggingface.co/markuz89/minijev-snake/resolve/main/code/minijev/export/onnx_export.py
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)) | |