NERVE / scripts /export_dynamic.py
Phips's picture
NERVE 1.80M release: models, optimized ONNX, ncnn, configs, arch, README
5a8f833 verified
Raw
History Blame Contribute Delete
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())