Download scripts/export_onnx.py from Dev2506/flash-crash-watchdog: direct link, hf CLI and curl.
- Browser
- Download file 2.85 kB
-
https://huggingface.co/Dev2506/flash-crash-watchdog/resolve/main/scripts/export_onnx.py
- Command line
-
hf download hf://Dev2506/flash-crash-watchdog/scripts/export_onnx.py
-
curl -L -o export_onnx.py https://huggingface.co/Dev2506/flash-crash-watchdog/resolve/main/scripts/export_onnx.py
2.85 kB
| #!/usr/bin/env python3 | |
| """NEW-03: export the Stage-3 TCN to ONNX + verify + benchmark. | |
| The TCN is a pure causal Conv1d stack, so it exports cleanly. The ONNX file is a | |
| plain (non-pickle) model artifact — it inherently sidesteps the weights_only | |
| (RCE) concern — and runs on onnxruntime, which is typically several x faster than | |
| the raw PyTorch forward (~11ms CPU). | |
| Verifies the exported ONNX output matches PyTorch, then times onnxruntime vs | |
| torch for a mini-batch. | |
| Usage: | |
| PYTHONPATH=ml python scripts/export_onnx.py \ | |
| --model models/stage3_tcn_prod.pt --out models/stage3_tcn_prod.onnx [--b 8] | |
| """ | |
| import argparse | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import onnxruntime as ort | |
| import torch | |
| ML_DIR = Path(__file__).resolve().parent.parent / "ml" | |
| sys.path.insert(0, str(ML_DIR)) | |
| from flash_crash_watchdog.models.stage3_tcn import TCNConfig, TCNDetector # noqa: E402 | |
| def main() -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--model", required=True) | |
| ap.add_argument("--out", required=True) | |
| ap.add_argument("--b", type=int, default=8, help="mini-batch for verify/benchmark") | |
| args = ap.parse_args() | |
| torch.serialization.add_safe_globals([TCNConfig]) # MLOPS-06 | |
| st = torch.load(args.model, map_location="cpu", weights_only=True) | |
| cfg = st["config"] if isinstance(st["config"], TCNConfig) else TCNConfig(**st["config"]) | |
| model = TCNDetector(cfg).to("cpu") | |
| model.load_state_dict(st["model_state"]) | |
| model.eval() | |
| dummy = torch.randn(args.b, cfg.input_dim, cfg.sequence_length) | |
| torch.onnx.export( | |
| model, dummy, args.out, | |
| input_names=["x"], output_names=["scores"], | |
| dynamic_axes={"x": {0: "batch"}, "scores": {0: "batch", 1: "time"}}, | |
| opset_version=14, | |
| dynamo=False, # legacy TorchScript exporter — no onnxscript dep | |
| ) | |
| print(f"[onnx] exported -> {args.out}") | |
| sess = ort.InferenceSession(args.out, providers=["CPUExecutionProvider"]) | |
| y_ort = sess.run(None, {"x": dummy.numpy()})[0] | |
| with torch.no_grad(): | |
| y_torch = model(dummy).numpy() | |
| diff = float(np.abs(y_ort - y_torch).max()) | |
| print(f"[onnx] max |ort - torch| = {diff:.2e} ({'OK' if diff < 1e-5 else 'MISMATCH'})") | |
| # benchmark onnxruntime vs torch (CPU) | |
| reps = 50 | |
| t0 = time.perf_counter() | |
| for _ in range(reps): | |
| sess.run(None, {"x": dummy.numpy()}) | |
| ort_ms = (time.perf_counter() - t0) / reps * 1000 | |
| t0 = time.perf_counter() | |
| with torch.no_grad(): | |
| for _ in range(reps): | |
| model(dummy) | |
| tor_ms = (time.perf_counter() - t0) / reps * 1000 | |
| print(f"[onnx] onnxruntime: {ort_ms:.3f} ms/batch of {args.b} | torch: {tor_ms:.3f} ms " | |
| f"({tor_ms / ort_ms:.1f}x)") | |
| return 0 if diff < 1e-5 else 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |