phungpx commited on
Commit
a15d07c
·
verified ·
1 Parent(s): 409ef8b

Upload ONNX export

Browse files
Files changed (1) hide show
  1. export_pp_doclayout_v3.py +215 -0
export_pp_doclayout_v3.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Export PaddlePaddle/PP-DocLayoutV3_safetensors to ONNX.
2
+
3
+ The HF module returns a large structured output (intermediate decoder states,
4
+ per-layer masks, reference points...). Only four tensors are needed at serving
5
+ time, so we wrap the model and return exactly those:
6
+
7
+ logits (B, 300, num_classes)
8
+ pred_boxes (B, 300, 4) cxcywh, normalised
9
+ order_logits (B, 300, 300) reading-order pointer matrix
10
+ out_masks (B, 300, 200, 200) mask logits @ stride 4 (optional)
11
+
12
+ `out_masks` is by far the biggest tensor (300*200*200*4B = 48 MB per image in
13
+ fp32). Export with --no-masks if you only need boxes + reading order; polygons
14
+ then degrade to axis-aligned rectangles.
15
+
16
+ Usage:
17
+ python export_pp_doclayout_v3.py --output pp_doclayoutv3.onnx
18
+ python export_pp_doclayout_v3.py --output pp_doclayoutv3_nomask.onnx --no-masks
19
+ python export_pp_doclayout_v3.py --output m.onnx --fp16 # extra fp16 copy
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ from contextlib import contextmanager
26
+ from pathlib import Path
27
+
28
+ import numpy as np
29
+ import torch
30
+ from torch import nn
31
+ from transformers import AutoModelForObjectDetection
32
+
33
+ DEFAULT_MODEL = "PaddlePaddle/PP-DocLayoutV3_safetensors"
34
+ INPUT_SIZE = 800 # preprocessor_config.json: {"height": 800, "width": 800}
35
+
36
+
37
+ @contextmanager
38
+ def fp32_position_embedding():
39
+ """Force the 2D sin/cos position embedding to float32 during tracing.
40
+
41
+ Upstream computes the frequency grid in float64, which bakes Sin/Cos(double)
42
+ nodes into the graph. ONNX Runtime's CPU EP has no double kernel for those, so
43
+ the session fails to load with:
44
+ NOT_IMPLEMENTED : Could not find an implementation for Cos(7)
45
+ The embedding is cast to float32 anyway; the difference is ~1e-6.
46
+ """
47
+ from transformers.models.pp_doclayout_v3 import modeling_pp_doclayout_v3 as M
48
+
49
+ original = M.build_2d_sinusoidal_position_embedding
50
+
51
+ def patched(height, width, embed_dim=256, temperature=10000.0,
52
+ cls_token=False, device=None, dtype=torch.float32):
53
+ pos_dim = embed_dim // 4
54
+ omega = torch.arange(pos_dim, dtype=torch.float32, device=device) / pos_dim
55
+ omega = 1.0 / temperature**omega
56
+ grid_h = torch.arange(height, dtype=torch.float32, device=device)
57
+ grid_w = torch.arange(width, dtype=torch.float32, device=device)
58
+ grid_h, grid_w = torch.meshgrid(grid_h, grid_w, indexing="ij")
59
+ emb_h = grid_h.flatten().outer(omega)
60
+ emb_w = grid_w.flatten().outer(omega)
61
+ pos_embed = torch.cat([emb_h.sin(), emb_h.cos(), emb_w.sin(), emb_w.cos()], dim=1)
62
+ if cls_token:
63
+ zeros = torch.zeros(1, embed_dim, dtype=torch.float32, device=device)
64
+ pos_embed = torch.cat([zeros, pos_embed], dim=0)
65
+ return pos_embed.to(dtype)
66
+
67
+ M.build_2d_sinusoidal_position_embedding = patched
68
+ try:
69
+ yield
70
+ finally:
71
+ M.build_2d_sinusoidal_position_embedding = original
72
+
73
+
74
+ class PPDocLayoutV3ExportWrapper(nn.Module):
75
+ """Flattens the HF output struct down to the tensors post-processing needs."""
76
+
77
+ def __init__(self, model: nn.Module, with_masks: bool = True) -> None:
78
+ super().__init__()
79
+ self.model = model
80
+ self.with_masks = with_masks
81
+
82
+ def forward(self, pixel_values: torch.Tensor):
83
+ out = self.model(pixel_values=pixel_values)
84
+
85
+ order_logits = out.order_logits
86
+ # Doc types it as a tuple; take the final decoder layer if so.
87
+ if isinstance(order_logits, (tuple, list)):
88
+ order_logits = order_logits[-1]
89
+ if order_logits.dim() == 4: # (B, layers, Q, Q)
90
+ order_logits = order_logits[:, -1]
91
+
92
+ if not self.with_masks:
93
+ return out.logits, out.pred_boxes, order_logits
94
+
95
+ masks = out.out_masks
96
+ if masks.dim() == 5: # (B, layers, Q, H, W) -> last layer only
97
+ masks = masks[:, -1]
98
+ return out.logits, out.pred_boxes, order_logits, masks
99
+
100
+
101
+ def export(
102
+ model_path: str,
103
+ output: Path,
104
+ with_masks: bool = True,
105
+ opset: int = 17,
106
+ dynamo: bool = False,
107
+ dynamic_batch: bool = True,
108
+ ) -> tuple[nn.Module, list[str]]:
109
+ model = AutoModelForObjectDetection.from_pretrained(model_path, dtype=torch.float32).eval()
110
+ # Pure-PyTorch deformable attention (grid_sample) instead of the custom CUDA
111
+ # kernel — the custom op has no ONNX symbolic. The checkpoint config already
112
+ # sets this, but be explicit in case someone overrides it.
113
+ model.config.disable_custom_kernels = True
114
+
115
+ wrapper = PPDocLayoutV3ExportWrapper(model, with_masks=with_masks).eval()
116
+
117
+ dummy = torch.randn(1, 3, INPUT_SIZE, INPUT_SIZE)
118
+ output_names = ["logits", "pred_boxes", "order_logits"]
119
+ if with_masks:
120
+ output_names.append("out_masks")
121
+
122
+ # Height/width stay static: the image processor always resizes to 800x800,
123
+ # and static spatial dims let ORT constant-fold the anchor generation.
124
+ dynamic_axes = None
125
+ if dynamic_batch:
126
+ dynamic_axes = {name: {0: "batch"} for name in ["pixel_values"] + output_names}
127
+
128
+ output.parent.mkdir(parents=True, exist_ok=True)
129
+ with torch.inference_mode(), fp32_position_embedding():
130
+ torch.onnx.export(
131
+ wrapper,
132
+ (dummy,),
133
+ str(output),
134
+ input_names=["pixel_values"],
135
+ output_names=output_names,
136
+ dynamic_axes=dynamic_axes,
137
+ opset_version=opset,
138
+ do_constant_folding=True,
139
+ dynamo=dynamo,
140
+ )
141
+ print(f"Exported -> {output} ({output.stat().st_size / 1e6:.1f} MB)")
142
+ return wrapper, output_names
143
+
144
+
145
+ def check_parity(
146
+ wrapper: nn.Module, onnx_path: Path, output_names: list[str], batch: int = 2, atol: float = 1e-3
147
+ ) -> None:
148
+ """Compare ONNX Runtime against PyTorch on random input.
149
+
150
+ Note: this is only meaningful with the real pretrained weights. With randomly
151
+ initialised weights the encoder emits thousands of identical proposal scores,
152
+ so TopK query selection is arbitrary and torch/ORT legitimately disagree.
153
+ """
154
+ import onnxruntime as ort
155
+
156
+ x = torch.randn(batch, 3, INPUT_SIZE, INPUT_SIZE)
157
+ with torch.inference_mode():
158
+ torch_out = [t.numpy() for t in wrapper(x)]
159
+
160
+ sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
161
+ onnx_out = sess.run(None, {"pixel_values": x.numpy()})
162
+
163
+ ok = True
164
+ for name, t, o in zip(output_names, torch_out, onnx_out):
165
+ diff = np.abs(t - o).max()
166
+ status = "OK " if diff < atol else "FAIL"
167
+ ok &= diff < atol
168
+ print(f" {status} {name:14s} shape={tuple(o.shape)} max|diff|={diff:.3e}")
169
+ print("Parity check passed\n" if ok else "Parity check FAILED — do not ship this graph\n")
170
+
171
+
172
+ def to_fp16(onnx_path: Path) -> Path:
173
+ """Half-precision copy. Keep normalisation/mask ops in fp32 to avoid overflow."""
174
+ import onnx
175
+ from onnxconverter_common import float16
176
+
177
+ model = onnx.load(str(onnx_path))
178
+ fp16_model = float16.convert_float_to_float16(
179
+ model, keep_io_types=True, op_block_list=["GridSample", "ReduceMean", "Pow", "Sqrt", "Div"]
180
+ )
181
+ out = onnx_path.with_name(onnx_path.stem + "_fp16.onnx")
182
+ onnx.save(fp16_model, str(out))
183
+ print(f"fp16 -> {out} ({out.stat().st_size / 1e6:.1f} MB)")
184
+ return out
185
+
186
+
187
+ def main() -> int:
188
+ p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
189
+ p.add_argument("--model", default=DEFAULT_MODEL)
190
+ p.add_argument("--output", type=Path, default=Path("pp_doclayoutv3.onnx"))
191
+ p.add_argument("--opset", type=int, default=17, help=">=16 required for GridSample")
192
+ p.add_argument("--no-masks", dest="with_masks", action="store_false")
193
+ p.add_argument("--static-batch", dest="dynamic_batch", action="store_false")
194
+ p.add_argument("--dynamo", action="store_true", help="use the TorchDynamo exporter")
195
+ p.add_argument("--fp16", action="store_true", help="also emit an fp16 copy")
196
+ p.add_argument("--skip-check", action="store_true")
197
+ args = p.parse_args()
198
+
199
+ wrapper, names = export(
200
+ args.model,
201
+ args.output,
202
+ with_masks=args.with_masks,
203
+ opset=args.opset,
204
+ dynamo=args.dynamo,
205
+ dynamic_batch=args.dynamic_batch,
206
+ )
207
+ if not args.skip_check:
208
+ check_parity(wrapper, args.output, names, batch=2 if args.dynamic_batch else 1)
209
+ if args.fp16:
210
+ to_fp16(args.output)
211
+ return 0
212
+
213
+
214
+ if __name__ == "__main__":
215
+ raise SystemExit(main())