tinydoc-5m / tinydoc /export.py
harryfrz's picture
tinydoc-5m v4: OmniDocBench EN text edit 0.117 (v3 0.127), inline maths as text, invoice summary tables
e24bf15 verified
Raw History Blame Contribute Delete
4.18 kB
"""Export trained components to ONNX for CPU inference with ONNX Runtime, and check parity.
python -m tinydoc.export --weights runs --out models
"""
import argparse
from pathlib import Path
import numpy as np
import torch
from .features import MAX_CHARS, NUM_FEATS
from .models import build, count_params
OPSET = 17
def sample_inputs(name):
g = torch.Generator().manual_seed(0)
if name == "detector":
return (torch.rand(1, 3, 640, 480, generator=g),), ["image"], ["kernel_logits"], {"image": {0: "batch", 2: "height", 3: "width"}}
if name == "recognizer":
x = torch.rand(2, 1, 32, 320, generator=g)
mask = torch.zeros(2, 80, dtype=torch.bool)
mask[1, 60:] = True
return (x, mask), ["image", "step_mask"], ["logits"], {"image": {0: "batch", 3: "width"}, "step_mask": {0: "batch", 1: "steps"}}
if name == "orientation":
return (torch.rand(4, 1, 192, 192, generator=g),), ["patches"], ["logits"], {"patches": {0: "batch"}}
n = 37
return ((torch.randint(1, 100, (1, n, MAX_CHARS), generator=g), torch.rand(1, n, NUM_FEATS, generator=g),
torch.rand(1, n, 4, generator=g) * 30, torch.ones(1, n)),
["char_ids", "feats", "rel_boxes", "valid"], ["field_logits", "rel_logits", "doc_logits"],
{"char_ids": {0: "batch", 1: "segments"}, "feats": {0: "batch", 1: "segments"},
"rel_boxes": {0: "batch", 1: "segments"}, "valid": {0: "batch", 1: "segments"}})
def alt_inputs(name):
"""Different shapes from the export sample, to prove the dynamic axes really are dynamic."""
g = torch.Generator().manual_seed(1)
if name == "detector":
return (torch.rand(1, 3, 320, 704, generator=g),)
if name == "recognizer":
return torch.rand(3, 1, 32, 512, generator=g), torch.zeros(3, 128, dtype=torch.bool)
if name == "orientation":
return (torch.rand(7, 1, 192, 192, generator=g),)
n = 120
return (torch.randint(1, 100, (1, n, MAX_CHARS), generator=g), torch.rand(1, n, NUM_FEATS, generator=g),
torch.rand(1, n, 4, generator=g) * 30, torch.ones(1, n))
def export_one(name, weights, out_dir, random_init=False):
import onnxruntime as ort
ck = None if random_init else torch.load(Path(weights) / name / "best.pt", map_location="cpu", weights_only=False)
model = build(name, **((ck or {}).get("arch") or {}))
if ck:
model.load_state_dict(ck["model"])
model.eval()
args, in_names, out_names, dyn = sample_inputs(name)
path = Path(out_dir) / f"{name}.onnx"
torch.onnx.export(model, args, str(path), input_names=in_names, output_names=out_names,
dynamic_axes=dyn, opset_version=OPSET, dynamo=False)
import onnx
proto = onnx.load(str(path))
meta = proto.metadata_props.add()
meta.key, meta.value = "trained_parameters", str(count_params(model))
onnx.save(proto, str(path))
sess = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
worst = 0.0
for inputs in (args, alt_inputs(name)):
with torch.no_grad():
ref = model(*inputs)
ref = ref if isinstance(ref, tuple) else (ref,)
got = sess.run(None, {n: t.numpy() for n, t in zip(in_names, inputs)})
worst = max(worst, max(float(np.abs(r.numpy() - g).max()) for r, g in zip(ref, got)))
size = path.stat().st_size / 1e6
print(f" {name:12s} params={count_params(model):>9,d} {size:5.1f} MB max |torch - onnx| = {worst:.2e}")
return worst
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--weights", default="runs")
ap.add_argument("--out", default="models")
ap.add_argument("--random-init", action="store_true", help="export untrained weights (pipeline testing)")
args = ap.parse_args()
Path(args.out).mkdir(parents=True, exist_ok=True)
worst = max(export_one(n, args.weights, args.out, args.random_init) for n in ("detector", "recognizer", "orientation", "extractor"))
if worst > 1e-3:
raise SystemExit(f"ONNX parity check failed (max diff {worst:.2e})")
print(f"exported to {args.out}/ (parity ok)")
if __name__ == "__main__":
main()