Download scripts/export_dynamic.py from Phips/NERVE: direct link, hf CLI and curl.
- Browser
- Download file 5.48 kB
-
https://huggingface.co/Phips/NERVE/resolve/24e68b9287cde6d28d94d577fc61ac903de1632f/scripts/export_dynamic.py
- Command line
-
hf download hf://Phips/NERVE@24e68b9287cde6d28d94d577fc61ac903de1632f/scripts/export_dynamic.py
-
curl -L -o export_dynamic.py https://huggingface.co/Phips/NERVE/resolve/24e68b9287cde6d28d94d577fc61ac903de1632f/scripts/export_dynamic.py
5.48 kB
| """Export NERVE (canonical, configurable size) or SPAN to dynamic-shape ONNX and verify. | |
| - fp32 export with dynamic H/W on the lq/sr tensors | |
| - onnx.checker | |
| - ONNX Runtime CPU inference vs PyTorch CPU (max abs diff) at several shapes: | |
| 48x48, 323x711, 720x1280. | |
| Random weights prove the graph is export-clean; pass a trained EMA checkpoint | |
| (--checkpoint) to validate an actual trained model. Canonical export is | |
| opset 20 (torch 2.14's dynamo exporter has an opset floor of 18; an opset-17 | |
| request still emits 18). | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from torch.export import Dim | |
| from traiNNer.archs.nerve_arch import nerve | |
| ROOT = Path(__file__).resolve().parents[2] | |
| OUT_ROOT = ROOT / "experiments" / "nerve" / "export" | |
| SHAPES: list[tuple[int, int]] = [(48, 48), (323, 711), (720, 1280)] | |
| def load_checkpoint(model: torch.nn.Module, path: Path) -> None: | |
| if path.suffix == ".safetensors": | |
| try: | |
| from safetensors.torch import load_file | |
| sd = load_file(str(path), device="cpu") | |
| except Exception: # noqa: BLE001 - legacy pickle with .safetensors name | |
| import io | |
| sd = torch.load( | |
| io.BytesIO(path.read_bytes()), map_location="cpu", weights_only=True | |
| ) | |
| else: | |
| sd = torch.load(path, map_location="cpu", weights_only=True) | |
| missing, unexpected = model.load_state_dict(sd, strict=True) | |
| if missing or unexpected: | |
| raise RuntimeError( | |
| f"checkpoint mismatch: missing={missing} unexpected={unexpected}" | |
| ) | |
| def build(arch: str, scale: int, dim: int, n_blocks: int) -> torch.nn.Module: | |
| if arch == "span": | |
| from traiNNer.archs.span_arch import span | |
| return span(scale=scale) | |
| return nerve(scale=scale, dim=dim, n_blocks=n_blocks) | |
| def export_onnx( | |
| model: torch.nn.Module, | |
| scale: int, | |
| opset: int, | |
| out_path: Path, | |
| ) -> None: | |
| model.eval() | |
| x = torch.randn(1, 3, 64, 64) | |
| dynamic_shapes = {"x": {2: Dim("H"), 3: Dim("W")}} | |
| torch.onnx.export( | |
| model, | |
| (x,), | |
| str(out_path), | |
| opset_version=opset, | |
| dynamic_shapes=dynamic_shapes, | |
| ) | |
| def verify_ort(model: torch.nn.Module, onnx_path: Path, scale: int, tol: float) -> dict: | |
| import onnx | |
| import onnxruntime as ort | |
| onnx.checker.check_model(onnx_path) | |
| sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"]) | |
| iname = sess.get_inputs()[0].name | |
| model.eval() | |
| results: dict[str, float] = {} | |
| with torch.no_grad(): | |
| for h, w in SHAPES: | |
| x = torch.randn(1, 3, h, w) | |
| ref = model(x) | |
| ort_out = sess.run(None, {iname: x.numpy()})[0] | |
| diff = float(np.abs(ref.numpy() - ort_out).max()) | |
| results[f"{h}x{w}"] = round(diff, 6) | |
| return results | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--arch", default="nerve", choices=["nerve", "span"]) | |
| parser.add_argument("--dim", type=int, default=48) | |
| parser.add_argument("--n-blocks", type=int, default=16) | |
| parser.add_argument( | |
| "--checkpoint", default=None, help="trained EMA safetensors to load" | |
| ) | |
| parser.add_argument( | |
| "--scale", type=int, default=4, help="scale when a checkpoint is given" | |
| ) | |
| parser.add_argument("--scales", nargs="+", type=int, default=[4, 2]) | |
| parser.add_argument("--opsets", nargs="+", type=int, default=[20]) | |
| parser.add_argument("--tol", type=float, default=1e-3) | |
| parser.add_argument("--out-root", default=str(OUT_ROOT)) | |
| args = parser.parse_args() | |
| scales = [args.scale] if args.checkpoint else args.scales | |
| overall_rc = 0 | |
| summary: dict = {} | |
| for scale in scales: | |
| model = build(args.arch, scale, args.dim, args.n_blocks) | |
| model.to("cpu") | |
| model.eval() | |
| if args.checkpoint: | |
| load_checkpoint(model, Path(args.checkpoint)) | |
| params = sum(p.numel() for p in model.parameters()) | |
| print(f"arch {args.arch} scale {scale} params={params:,}", flush=True) | |
| scale_rc = 0 | |
| for opset in args.opsets: | |
| tag = f"{args.arch}_d{args.dim}_b{args.n_blocks}_s{scale}" | |
| out_dir = Path(args.out_root) / tag | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| out_path = out_dir / f"nerve_dynamic_op{opset}.onnx" | |
| export_onnx(model, scale, opset, out_path) | |
| print(f"exported {out_path.name}", flush=True) | |
| try: | |
| diffs = verify_ort(model, out_path, scale, args.tol) | |
| except Exception as e: # noqa: BLE001 | |
| print(f"opset {opset} ORT verify FAILED: {e}", flush=True) | |
| scale_rc = 1 | |
| continue | |
| worst = max(diffs.values()) if diffs else float("inf") | |
| ok = worst <= args.tol | |
| print( | |
| f"opset {opset} max|diff| per shape: {diffs} -> {'OK' if ok else 'FAIL'}", | |
| flush=True, | |
| ) | |
| scale_rc |= 0 if ok else 1 | |
| summary[f"arch{args.arch}_s{scale}_op{opset}"] = { | |
| "max_diff": diffs, | |
| "ok": ok, | |
| } | |
| overall_rc |= scale_rc | |
| (Path(args.out_root) / "export_summary.json").write_text( | |
| json.dumps(summary, indent=2) | |
| ) | |
| return overall_rc | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |