psiddh commited on
Commit
c5a149d
·
verified ·
1 Parent(s): a9ed467

Add ExecuTorch WebGPU program for dfine-small

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ dfine_s_coco_webgpu.pte filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
NOTICE ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ This repository contains a modified version of ustc-community/dfine-small-coco.
2
+
3
+ Modifications by Meta Platforms, Inc.: conversion to the ExecuTorch program format (.pte) for the
4
+ ExecuTorch WebGPU backend.
5
+
6
+ The original work and these modifications are distributed under the license in LICENSE.
README.md ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model:
4
+ - ustc-community/dfine-small-coco
5
+ library_name: executorch
6
+ pipeline_tag: object-detection
7
+ inference: false
8
+ tags:
9
+ - executorch
10
+ - webgpu
11
+ - on-device
12
+ - browser
13
+ ---
14
+ # D-FINE small object detection for ExecuTorch WebGPU
15
+
16
+ This repository contains an [ExecuTorch](https://github.com/pytorch/executorch) conversion of
17
+ [ustc-community/dfine-small-coco](https://huggingface.co/ustc-community/dfine-small-coco) for the ExecuTorch **WebGPU** backend, so the model runs entirely in a web browser on
18
+ the visitor's GPU. It powers the corresponding demo in the ExecuTorch WebGPU Playground (launching soon on [executorch.ai](https://executorch.ai)).
19
+
20
+ - **Base model:** [ustc-community/dfine-small-coco](https://huggingface.co/ustc-community/dfine-small-coco)
21
+ - **Format:** ExecuTorch `.pte` programs delegated to the WebGPU backend
22
+ - **Quantization:** fp32 (as exported)
23
+ - **Total size:** 40.3 MB
24
+
25
+ > **Note:** The weights are the Hugging Face transformers port of the authors' checkpoint; every tensor was checked to be bit-identical to the official `dfine_s_coco.pth` published by the first author (see Provenance and `export/verify_official.py`).
26
+
27
+ ## About the original model
28
+
29
+ The small COCO model of D-FINE, a real-time DETR-style object detector that refines boxes as probability distributions.
30
+
31
+ - **Developed by:** Yansong Peng et al. (USTC)
32
+ - **Paper:** [D-FINE: Redefine Regression Task in DETRs as Fine-grained Distribution Refinement](https://arxiv.org/abs/2410.13842)
33
+ - 10.3M parameters
34
+ - Detects the 80 COCO object classes
35
+ - No non-maximum suppression needed
36
+
37
+ For training data, evaluation results and full details, see the original model card: [ustc-community/dfine-small-coco](https://huggingface.co/ustc-community/dfine-small-coco).
38
+
39
+ ## Intended use and limitations
40
+
41
+ Detecting everyday objects in photos, video and camera frames.
42
+
43
+ Only knows the 80 COCO classes. Small, unusual or heavily occluded objects are often missed, and confidence scores are not calibrated probabilities.
44
+
45
+ ## What changed in this conversion
46
+
47
+ - Graph rewritten with output-preserving substitutions so it runs as a single WebGPU delegate (checked against upstream on a real image: boxes within 0.001 px)
48
+ - Fixed 640x640 input; the 300 query outputs are filtered by score outside the program
49
+
50
+ ## Inputs and outputs
51
+
52
+ - Input `[1, 3, 640, 640]` float32 RGB in [0, 1] (`x / 255`, no mean/std); letterboxed or plain-resized
53
+ - Output `[1, 84, 300]` float32: rows 0-3 are box center x, center y, width, height in input pixels; rows 4-83 are sigmoid scores for the 80 COCO classes
54
+
55
+ ## Files
56
+
57
+ | File | Size | SHA-256 |
58
+ |---|---|---|
59
+ | `dfine_s_coco_webgpu.pte` | 40.3 MB | `96cc65f615dc2c79…` |
60
+
61
+ Full checksums are in `manifest.json`. The playground verifies them after download.
62
+
63
+ ## Running it
64
+
65
+ These programs were exported with the ExecuTorch WebGPU backend
66
+ ([backends/webgpu](https://github.com/pytorch/executorch/tree/main/backends/webgpu)) and validated with the
67
+ WebAssembly runtime build used by the ExecuTorch WebGPU Playground (launching soon on [executorch.ai](https://executorch.ai)). That runtime build is not published yet; until it is,
68
+ the playground is the supported way to run them. It downloads these files once, caches them in the browser,
69
+ and runs everything locally.
70
+
71
+ Requirements: a browser with WebGPU enabled.
72
+
73
+ ## Provenance
74
+
75
+ - **Weights:** `ustc-community/dfine-small-coco` @ `f79e65b5fbb33ceb9d3ebba042955d7410c608f8`, file `model.safetensors`, sha256 `a144baff3a27bfd538fe37b11fa8d4fca5744b8c054b5301e7c699a1379cd18f` (from a community mirror of the official release)
76
+ - **Checked against the official checkpoint:** `Peterande/D-FINE` @ `19214e91f5ce4c1e609022c38b1fc953c47404e7`, file `dfine_s_coco.pth` (sha256 `48a6c8cc43eb57186843f752e2e8461ddd3326e0d3c575e71e6e960844683e89`): all 764 tensors bit-identical (verify_official.py)
77
+ - **Exported with:** executorch 1.5.1+cpu, torch 2.14.1+cpu, torchao 0.18.0+cpu, transformers 5.19.0 (executorch git `3b6068392324`)
78
+ - **Reproduce:** `python recipes/detector-dfine-small.py --out out/detector-dfine-small --image photo.jpg` (recipe in `export/`)
79
+ - Re-running the recipe produces byte-identical files.
80
+
81
+ ## Citation
82
+
83
+ If you use this model, please cite the original work:
84
+
85
+ ```bibtex
86
+ @misc{peng2024dfine,
87
+ title={D-FINE: Redefine Regression Task in DETRs as Fine-grained Distribution Refinement},
88
+ author={Yansong Peng and Hebei Li and Peixi Wu and Yueyi Zhang and Xiaoyan Sun and Feng Wu},
89
+ year={2024},
90
+ eprint={2410.13842},
91
+ archivePrefix={arXiv},
92
+ primaryClass={cs.CV}
93
+ }
94
+ ```
95
+
96
+ ## License
97
+
98
+ This is a derivative of the upstream model and is distributed under the same terms
99
+ (`apache-2.0`); see `LICENSE`.
100
+ Modifications (conversion to the ExecuTorch format) by Meta Platforms, Inc.
dfine_s_coco_webgpu.pte ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96cc65f615dc2c79d1da80ef075b831fac8e39e8dcb58af9aff5bab154a4c4d6
3
+ size 42260612
export/_common.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared helpers for the ExecuTorch WebGPU export recipes in this directory.
2
+
3
+ Each recipe builds an eager model from a pinned upstream revision and calls
4
+ `export_vision` (or its own LLM path), which writes:
5
+
6
+ <out>/<name>_webgpu.pte program lowered with WebGPUPartitioner (delegate id "VulkanBackend")
7
+ <out>/<name>_portable.pte same exported graph without a delegate, for CPU validation only
8
+ <out>/recipe.json upstream id + revision, versions, shapes, dtype, command line
9
+ <out>/validation.json eager vs exported-program vs portable .pte (and goldens, if given)
10
+ """
11
+
12
+ import hashlib
13
+ import io
14
+ import json
15
+ import platform
16
+ import shlex
17
+ import sys
18
+ import urllib.request
19
+ import zipfile
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+ import torch
24
+
25
+ PROXY_HINT = (
26
+ "set https_proxy if the machine needs a proxy for huggingface.co / pypi.org"
27
+ )
28
+
29
+
30
+ def versions():
31
+ import executorch
32
+ import torchao
33
+ from executorch.version import git_version
34
+
35
+ out = {
36
+ "python": platform.python_version(),
37
+ "torch": torch.__version__,
38
+ "torchao": torchao.__version__,
39
+ "executorch": getattr(executorch, "__version__", None)
40
+ or __import__("executorch.version").version.__version__,
41
+ "executorch_git": git_version,
42
+ }
43
+ try:
44
+ import transformers
45
+
46
+ out["transformers"] = transformers.__version__
47
+ except ImportError:
48
+ pass
49
+ try:
50
+ import huggingface_hub
51
+
52
+ out["huggingface_hub"] = huggingface_hub.__version__
53
+ except ImportError:
54
+ pass
55
+ return out
56
+
57
+
58
+ def sha256_file(path):
59
+ h = hashlib.sha256()
60
+ with open(path, "rb") as fh:
61
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
62
+ h.update(chunk)
63
+ return h.hexdigest()
64
+
65
+
66
+ def hf_file(repo_id, filename, revision, expected_sha256=None, cache_dir=None):
67
+ from huggingface_hub import hf_hub_download
68
+
69
+ path = hf_hub_download(
70
+ repo_id=repo_id, filename=filename, revision=revision, cache_dir=cache_dir
71
+ )
72
+ digest = sha256_file(path)
73
+ if expected_sha256 and digest != expected_sha256:
74
+ sys.exit(
75
+ f"{repo_id}@{revision}:{filename} sha256 {digest} != {expected_sha256}"
76
+ )
77
+ return Path(path), digest
78
+
79
+
80
+ def pypi_wheel_member(url, expected_sha256, member):
81
+ """Read one file out of a PyPI wheel, verifying the wheel's sha256."""
82
+ with urllib.request.urlopen(url, timeout=120) as resp:
83
+ blob = resp.read()
84
+ digest = hashlib.sha256(blob).hexdigest()
85
+ if digest != expected_sha256:
86
+ sys.exit(f"{url}: sha256 {digest} != {expected_sha256} ({PROXY_HINT})")
87
+ with zipfile.ZipFile(io.BytesIO(blob)) as zf:
88
+ return zf.read(member).decode()
89
+
90
+
91
+ def scrub(text, out_dir):
92
+ """Replace machine-specific paths so recipe.json can ship with the model."""
93
+ text = text.replace(str(Path(out_dir).resolve()), "<out>")
94
+ text = text.replace(str(Path(__file__).resolve().parent), "recipes")
95
+ return text.replace(str(Path.home()), "~")
96
+
97
+
98
+ def command_line(out_dir):
99
+ argv = [f"recipes/{Path(sys.argv[0]).name}", *sys.argv[1:]]
100
+ args = []
101
+ for a in argv:
102
+ if a.startswith("/") or a.startswith("."):
103
+ a = scrub(str(Path(a).resolve()), out_dir)
104
+ args.append(a)
105
+ return scrub(" ".join(["python", *(shlex.quote(a) for a in args)]), out_dir)
106
+
107
+
108
+ def lower_webgpu(ep):
109
+ from executorch.backends.webgpu.partitioner import WebGPUPartitioner
110
+ from executorch.exir import to_edge_transform_and_lower
111
+
112
+ return to_edge_transform_and_lower(
113
+ ep, partitioner=[WebGPUPartitioner()]
114
+ ).to_executorch()
115
+
116
+
117
+ def lower_portable(ep):
118
+ from executorch.exir import to_edge_transform_and_lower
119
+
120
+ return to_edge_transform_and_lower(ep).to_executorch()
121
+
122
+
123
+ def delegate_summary(pte_bytes):
124
+ """Non-delegated kernels and per-delegate op histograms of a serialized program."""
125
+ import collections
126
+
127
+ from executorch.backends.vulkan.serialization.vulkan_graph_serialize import (
128
+ extract_vk_flatbuffer,
129
+ flatbuffer_to_vk_graph,
130
+ )
131
+ from executorch.exir._serialize._program import deserialize_pte_binary
132
+
133
+ pte = deserialize_pte_binary(pte_bytes)
134
+ prog = pte.program if hasattr(pte, "program") else pte
135
+ seg_base = int.from_bytes(pte_bytes[24:32], "little")
136
+ plan = prog.execution_plan[0]
137
+ ops = [f"{o.name}.{o.overload}" for o in plan.operators]
138
+ kernels = collections.Counter()
139
+ for chain in plan.chains:
140
+ for ins in chain.instructions:
141
+ if type(ins.instr_args).__name__ == "KernelCall":
142
+ kernels[ops[ins.instr_args.op_index]] += 1
143
+ delegates = []
144
+ for d in plan.delegates:
145
+ proc = d.processed
146
+ if proc.location.name == "INLINE":
147
+ blob = prog.backend_delegate_data[proc.index].data
148
+ else:
149
+ seg = prog.segments[proc.index]
150
+ blob = pte_bytes[seg_base + seg.offset : seg_base + seg.offset + seg.size]
151
+ g = flatbuffer_to_vk_graph(extract_vk_flatbuffer(blob))
152
+ delegates.append(
153
+ {
154
+ "id": d.id,
155
+ "ops": dict(collections.Counter(c.name for c in g.chain)),
156
+ "inputs": len(g.input_ids),
157
+ "outputs": len(g.output_ids),
158
+ }
159
+ )
160
+ return {"non_delegated_kernels": dict(kernels), "delegates": delegates}
161
+
162
+
163
+ def compare(a, b):
164
+ a = np.asarray(a, dtype=np.float64).ravel()
165
+ b = np.asarray(b, dtype=np.float64).ravel()
166
+ n = min(a.size, b.size)
167
+ a, b = a[:n], b[:n]
168
+ cos = float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-30))
169
+ return {
170
+ "max_abs": float(np.max(np.abs(a - b))),
171
+ "mean_abs": float(np.mean(np.abs(a - b))),
172
+ "cosine": cos,
173
+ }
174
+
175
+
176
+ def run_pte(pte_bytes, inputs):
177
+ from executorch.extension.pybindings.portable_lib import (
178
+ _load_for_executorch_from_buffer,
179
+ )
180
+
181
+ m = _load_for_executorch_from_buffer(pte_bytes)
182
+ return [
183
+ o.detach().numpy() if isinstance(o, torch.Tensor) else o
184
+ for o in m.forward(inputs)
185
+ ]
186
+
187
+
188
+ def try_run_webgpu_pte_on_cpu(pte_bytes, inputs):
189
+ try:
190
+ return run_pte(pte_bytes, inputs), None
191
+ except Exception as e: # noqa: BLE001 - the error text is the result
192
+ return None, f"{type(e).__name__}: {str(e).splitlines()[0][:300]}"
193
+
194
+
195
+ def export_vision(
196
+ *,
197
+ name,
198
+ model,
199
+ example_input,
200
+ out_dir,
201
+ upstream,
202
+ extra_recipe,
203
+ golden=None,
204
+ output_index=None,
205
+ ):
206
+ """Export, lower, write both .pte files, recipe.json, and validation.json.
207
+
208
+ `golden` is an optional (input ndarray, output ndarray) pair; when its input
209
+ shape matches `example_input`, all paths are also compared against it.
210
+ `output_index` picks one tensor when the eager model returns a tuple.
211
+ """
212
+ out_dir = Path(out_dir)
213
+ out_dir.mkdir(parents=True, exist_ok=True)
214
+ torch.manual_seed(0)
215
+ model = model.eval()
216
+
217
+ class Wrapped(torch.nn.Module):
218
+ def __init__(self, m):
219
+ super().__init__()
220
+ self.m = m
221
+
222
+ def forward(self, x):
223
+ y = self.m(x)
224
+ return y[output_index] if output_index is not None else y
225
+
226
+ wrapped = Wrapped(model).eval()
227
+ with torch.no_grad():
228
+ ep = torch.export.export(wrapped, (example_input,), strict=False)
229
+ webgpu = lower_webgpu(ep)
230
+ portable = lower_portable(ep)
231
+
232
+ webgpu_path = out_dir / f"{name}_webgpu.pte"
233
+ portable_path = out_dir / f"{name}_portable.pte"
234
+ webgpu_path.write_bytes(webgpu.buffer)
235
+ portable_path.write_bytes(portable.buffer)
236
+
237
+ summary = delegate_summary(webgpu.buffer)
238
+ out_shape = list(ep.module()(example_input).shape)
239
+ recipe = {
240
+ "model": name,
241
+ "upstream": upstream,
242
+ "versions": versions(),
243
+ "export": {
244
+ "api": "torch.export.export(strict=False) -> to_edge_transform_and_lower(partitioner=[WebGPUPartitioner()]) -> to_executorch()",
245
+ "partitioner": "executorch.backends.webgpu.partitioner.WebGPUPartitioner() (default compile options; wraps VulkanPartitioner)",
246
+ "inputs": [
247
+ {
248
+ "shape": list(example_input.shape),
249
+ "dtype": "float32",
250
+ "layout": "NCHW",
251
+ }
252
+ ],
253
+ "outputs": [{"shape": out_shape, "dtype": "float32"}],
254
+ "dtype": "float32",
255
+ "quantization": "none (fp32)",
256
+ **extra_recipe,
257
+ },
258
+ "files": {
259
+ webgpu_path.name: {
260
+ "bytes": webgpu_path.stat().st_size,
261
+ "sha256": sha256_file(webgpu_path),
262
+ },
263
+ portable_path.name: {
264
+ "bytes": portable_path.stat().st_size,
265
+ "sha256": sha256_file(portable_path),
266
+ "note": "CPU-only validation artifact (portable kernels, no delegate); not for release",
267
+ },
268
+ },
269
+ "graph": summary,
270
+ "command": command_line(out_dir),
271
+ }
272
+ (out_dir / "recipe.json").write_text(json.dumps(recipe, indent=2) + "\n")
273
+
274
+ # Validation on CPU.
275
+ gen = torch.Generator().manual_seed(1234)
276
+ rand_input = torch.rand(example_input.shape, generator=gen)
277
+ cases = {"random": rand_input}
278
+ if golden is not None and tuple(golden[0].shape) == tuple(example_input.shape):
279
+ cases["golden"] = torch.from_numpy(golden[0].copy())
280
+ report = {
281
+ "cpu_runtime": "executorch pybindings (portable_lib) from the pip wheel",
282
+ "cases": {},
283
+ }
284
+ webgpu_cpu_error = None
285
+ for label, x in cases.items():
286
+ with torch.no_grad():
287
+ eager = wrapped(x).numpy()
288
+ exported = ep.module()(x).numpy()
289
+ port = run_pte(portable.buffer, (x,))[0]
290
+ res = {
291
+ "exported_vs_eager": compare(exported, eager),
292
+ "portable_pte_vs_eager": compare(port, eager),
293
+ }
294
+ wg, err = try_run_webgpu_pte_on_cpu(webgpu.buffer, (x,))
295
+ if wg is not None:
296
+ res["webgpu_pte_vs_eager"] = compare(wg[0], eager)
297
+ else:
298
+ webgpu_cpu_error = err
299
+ if label == "golden":
300
+ ref = golden[1]
301
+ res["internal_golden_vs_eager"] = compare(eager, ref)
302
+ res["internal_golden_vs_portable_pte"] = compare(port, ref)
303
+ res["golden_note"] = (
304
+ "reference output from the internal goldens.manifest.json"
305
+ )
306
+ report["cases"][label] = res
307
+ report["webgpu_pte_on_cpu"] = (
308
+ "ran"
309
+ if webgpu_cpu_error is None
310
+ else f"not runnable on CPU pybindings: {webgpu_cpu_error}"
311
+ )
312
+ (out_dir / "validation.json").write_text(json.dumps(report, indent=2) + "\n")
313
+ print(
314
+ json.dumps(
315
+ {"recipe": recipe["files"], "graph": summary, "validation": report},
316
+ indent=2,
317
+ )
318
+ )
319
+ return ep, webgpu, portable
320
+
321
+
322
+ def load_golden(manifest_path, key):
323
+ man = json.loads(Path(manifest_path).read_text())[key]["tensors"]
324
+ base = Path(manifest_path).parent
325
+
326
+ def ld(t):
327
+ return np.fromfile(base / t["file"], dtype=np.float32).reshape(t["shape"])
328
+
329
+ inp = man.get("input")
330
+ return ld(inp), ld(man["output"])
export/_detector.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Graph building blocks for exporting DETR-style detectors to the playground's WebGPU runtime.
2
+
3
+ The playground's current `webgpu_forward.wasm` registers 63 operators and links no CPU kernels,
4
+ so a program must be one delegate made only of those operators, with every elementwise tensor
5
+ at rank <= 4. These helpers rewrite the pieces of RT-DETR / D-FINE that fall outside that set
6
+ using only: add/sub/mul/div, relu, sigmoid, where.self, gt/ge/le/lt.Scalar, sum.dim,
7
+ gather (fp32 self + int32 index), _to_copy (fp32 <-> int32), view/permute/expand/slice/cat.
8
+
9
+ Each helper is exact (selection by comparison, integer arithmetic below 2^24) or an
10
+ algebraic identity of the original op; see the docstrings.
11
+ """
12
+
13
+ import torch
14
+
15
+
16
+ def clamp(x, lo=None, hi=None):
17
+ """Exact clamp written with where + compare-against-0.
18
+
19
+ The current runtime's aten.clamp returns zeros for integer-valued bounds (the serializer
20
+ stores them as Int), so clamp is never emitted. Bounds are lifted [1] constants.
21
+ """
22
+ if lo is not None:
23
+ x = torch.where((x - lo) < 0, torch.tensor([float(lo)]), x)
24
+ if hi is not None:
25
+ x = torch.where((x - hi) > 0, torch.tensor([float(hi)]), x)
26
+ return x
27
+
28
+
29
+ def where_max(a, b):
30
+ """Elementwise max(a, b), exact: picks b where b - a > 0 (a - b is never 0 for a != b)."""
31
+ return torch.where((b - a) > 0, b, a)
32
+
33
+
34
+ def where_min(a, b):
35
+ return torch.where((b - a) < 0, b, a)
36
+
37
+
38
+ def tree_reduce(x, pick):
39
+ """Reduce the last dim with a pairwise `pick` (where_max / where_min); keeps the dim (size 1)."""
40
+ while x.shape[-1] > 1:
41
+ n = x.shape[-1]
42
+ h = n // 2
43
+ m = pick(x[..., :h], x[..., h : 2 * h])
44
+ x = torch.cat([m, x[..., 2 * h :]], dim=-1) if n % 2 else m
45
+ return x
46
+
47
+
48
+ class TopKSet(torch.nn.Module):
49
+ """Indices of the k largest entries of a [N] score vector, in ascending index order.
50
+
51
+ torch.topk is not available on the runtime. The detectors' decoders are permutation
52
+ equivariant over queries (self-attention with box-derived position embeddings, per-query
53
+ deformable cross-attention), so only the selected *set* matters, not its order.
54
+
55
+ 1. Bisection for the k-th largest value t: `iters` steps of count(s >= mid) >= k.
56
+ After enough steps lo is the k-th largest value (unless another score lies within
57
+ (hi - lo) / 2^iters of it), so {s >= lo} is exactly the top-k set (ties aside).
58
+ 2. Stream compaction: inclusive prefix count of the selection mask (two-level block cumsum
59
+ done with a triangular mask and sum, exact for integers), one-hot match of the
60
+ positions 0..k-1 with relu(1 - d^2), and index = sum(onehot * arange) (exact, < 2^24).
61
+ Returns a float tensor [k] of integer-valued indices.
62
+ """
63
+
64
+ def __init__(self, n, k, iters=48, block=None):
65
+ super().__init__()
66
+ self.n, self.k, self.iters = n, k, iters
67
+ if block is None:
68
+ block = next(b for b in range(int(n**0.5), 0, -1) if n % b == 0)
69
+ self.block = block
70
+ nb = n // block
71
+ self.nb = nb
72
+ self.register_buffer(
73
+ "tri_in", torch.tril(torch.ones(block, block)), persistent=False
74
+ )
75
+ self.register_buffer(
76
+ "tri_blk", torch.tril(torch.ones(nb, nb), diagonal=-1), persistent=False
77
+ )
78
+ self.register_buffer("one", torch.ones(1), persistent=False)
79
+ self.register_buffer("zero", torch.zeros(1), persistent=False)
80
+ self.register_buffer(
81
+ "slots", torch.arange(k, dtype=torch.float32).view(k, 1), persistent=False
82
+ )
83
+ self.register_buffer(
84
+ "arange", torch.arange(n, dtype=torch.float32).view(1, n), persistent=False
85
+ )
86
+
87
+ def forward(self, s): # s: [1, N]
88
+ lo = tree_reduce(s, where_min) - 1.0 # [1, 1]
89
+ hi = tree_reduce(s, where_max) + 1.0
90
+ for _ in range(self.iters):
91
+ mid = (lo + hi) * 0.5
92
+ cnt = torch.where((s - mid) >= 0, self.one, self.zero).sum(
93
+ dim=-1, keepdim=True
94
+ )
95
+ ok = (cnt - self.k) >= 0
96
+ lo = torch.where(ok, mid, lo)
97
+ hi = torch.where(ok, hi, mid)
98
+ sel = torch.where((s - lo) >= 0, self.one, self.zero) # [1, N]
99
+ blocks = sel.view(self.nb, 1, self.block)
100
+ within = (blocks * self.tri_in).sum(
101
+ dim=-1
102
+ ) # [nb, block] inclusive count in block
103
+ totals = blocks.sum(dim=-1).view(1, self.nb) # [1, nb]
104
+ offsets = (
105
+ (totals * self.tri_blk).sum(dim=-1).view(self.nb, 1)
106
+ ) # exclusive block prefix
107
+ pos = (
108
+ sel * (within + offsets).view(1, self.n)
109
+ ) - 1.0 # selected -> 0.., others -> -1
110
+ d = pos - self.slots # [k, N]
111
+ onehot = torch.relu(1.0 - d * d)
112
+ return (onehot * self.arange).sum(dim=-1) # [k]
113
+
114
+
115
+ def sigmoid_add_logit(delta, r, eps=1e-5):
116
+ """sigmoid(delta + inverse_sigmoid(r)) without log/exp.
117
+
118
+ With x = clamp(r, 0, 1), x1 = clamp(x, eps), x2 = clamp(1 - x, eps) (as in
119
+ inverse_sigmoid), sigmoid(delta + log(x1 / x2)) = x1 s / (x1 s + x2 (1 - s)) with
120
+ s = sigmoid(delta) and 1 - s = sigmoid(-delta).
121
+ """
122
+ x = clamp(r, 0.0, 1.0)
123
+ x1 = clamp(x, eps)
124
+ x2 = clamp(1.0 - x, eps)
125
+ a = x1 * torch.sigmoid(delta)
126
+ return a / (a + x2 * torch.sigmoid(-delta))
127
+
128
+
129
+ class DeformSampler(torch.nn.Module):
130
+ """Multi-scale deformable attention core with bilinear grid_sample written as gathers.
131
+
132
+ Matches F.grid_sample(mode="bilinear", padding_mode="zeros", align_corners=False) on grid
133
+ g = 2 * loc - 1, pixel x = ((g + 1) * W - 1) / 2. floor() comes from an fp32 -> int32 cast of a
134
+ clamped, non-negative value; out-of-range corners get weight 0 through an integer mask
135
+ clamp(x0 + 1, 0, 1) * clamp(W - x0, 0, 1). Index clamping to [0, n-1] with a per-point n uses
136
+ min(i - (n - 1), 0) + (n - 1), exact on small integers (clamp.Tensor is not available).
137
+ All levels are concatenated along the spatial axis so one gather serves every
138
+ (level, point, corner).
139
+
140
+ points_per_level: number of sampling points per level, e.g. [6, 6] or [4, 4, 4].
141
+ shapes: [(H, W), ...] per level.
142
+ """
143
+
144
+ def __init__(self, shapes, points_per_level, heads, head_dim):
145
+ super().__init__()
146
+ self.heads, self.head_dim = heads, head_dim
147
+ self.P = sum(points_per_level)
148
+ self.L = sum(h * w for h, w in shapes)
149
+ Ws, Hs, starts = [], [], []
150
+ start = 0
151
+ for (h, w), p in zip(shapes, points_per_level):
152
+ Ws += [float(w)] * p
153
+ Hs += [float(h)] * p
154
+ starts += [float(start)] * p
155
+ start += h * w
156
+ self.wmax, self.hmax = max(Ws), max(Hs)
157
+ # Shaped [P, 1]: coordinates keep a trailing size-1 dim (slice, not select; the current
158
+ # runtime rejects a select index that the serializer stored as a Double).
159
+ self.register_buffer("W", torch.tensor(Ws).view(-1, 1), persistent=False)
160
+ self.register_buffer("H", torch.tensor(Hs).view(-1, 1), persistent=False)
161
+ self.register_buffer("start", torch.tensor(starts).view(-1, 1), persistent=False)
162
+
163
+ def forward(self, value, loc, attn):
164
+ """value [L, heads, head_dim]; loc [Q, heads, P, 2] in [0, 1]; attn [Q, heads, P].
165
+
166
+ Returns [Q, heads * head_dim].
167
+ """
168
+ Q = loc.shape[0]
169
+ nh, hd, P = self.heads, self.head_dim, self.P
170
+ loc = loc.permute(1, 0, 2, 3) # [heads, Q, P, 2]
171
+ W, H = self.W, self.H
172
+ # Same arithmetic as grid_sampler's unnormalize on grid = 2 * loc - 1.
173
+ x = ((loc[..., 0:1] * 2.0 - 1.0 + 1.0) * W - 1.0) / 2.0 # [heads, Q, P, 1]
174
+ y = ((loc[..., 1:2] * 2.0 - 1.0 + 1.0) * H - 1.0) / 2.0
175
+ # trunc == floor once x + 1 >= 0. A point left of -1 or right of the level's W has
176
+ # both corners outside, so clamping to [-1, max W] cannot change the result.
177
+ x = clamp(x, -1.0, self.wmax)
178
+ y = clamp(y, -1.0, self.hmax)
179
+ x0 = (x + 1.0).to(torch.int32).to(torch.float32) - 1.0 # floor: x + 1 >= 0
180
+ y0 = (y + 1.0).to(torch.int32).to(torch.float32) - 1.0
181
+ fx = x - x0
182
+ fy = y - y0
183
+ x1 = x0 + 1.0
184
+ y1 = y0 + 1.0
185
+
186
+ def inside(i, n):
187
+ return clamp(i + 1.0, 0.0, 1.0) * clamp(n - i, 0.0, 1.0)
188
+
189
+ def clampi(i, n):
190
+ return clamp(clamp(i, 0.0) - (n - 1.0), None, 0.0) + (n - 1.0)
191
+
192
+ mx0, mx1, my0, my1 = inside(x0, W), inside(x1, W), inside(y0, H), inside(y1, H)
193
+ cx0, cx1 = clampi(x0, W), clampi(x1, W)
194
+ ry0 = clampi(y0, H) * W + self.start
195
+ ry1 = clampi(y1, H) * W + self.start
196
+ a = attn.permute(1, 0, 2).reshape(nh, Q, P, 1)
197
+ w00 = (1.0 - fx) * (1.0 - fy) * mx0 * my0 * a
198
+ w01 = fx * (1.0 - fy) * mx1 * my0 * a
199
+ w10 = (1.0 - fx) * fy * mx0 * my1 * a
200
+ w11 = fx * fy * mx1 * my1 * a
201
+ idx = torch.cat([ry0 + cx0, ry0 + cx1, ry1 + cx0, ry1 + cx1], dim=-1) # [heads, Q, P, 4]
202
+ wts = torch.cat([w00, w01, w10, w11], dim=-1)
203
+ K = Q * P * 4
204
+ idx = idx.reshape(nh, 1, K).expand(nh, hd, K).to(torch.int64)
205
+ v = value.permute(1, 2, 0) # [heads, head_dim, L]
206
+ g = torch.gather(v, 2, idx) # [heads, head_dim, K]
207
+ out = (
208
+ (g * wts.reshape(nh, 1, K)).reshape(nh, hd, Q, P * 4).sum(dim=-1)
209
+ ) # [heads, hd, Q]
210
+ return out.permute(2, 0, 1).reshape(Q, nh * hd)
211
+
212
+
213
+ def attention(q_proj, k_proj, v_proj, o_proj, hidden, pos, heads):
214
+ """Self-attention with position embeddings on queries/keys (DETR style). hidden [1, N, C]."""
215
+ n, c = hidden.shape[1], hidden.shape[2]
216
+ hd = c // heads
217
+ qk_in = hidden + pos
218
+ q = q_proj(qk_in).view(n, heads, hd).permute(1, 0, 2)
219
+ k = k_proj(qk_in).view(n, heads, hd).permute(1, 2, 0)
220
+ v = v_proj(hidden).view(n, heads, hd).permute(1, 0, 2)
221
+ w = torch.softmax(torch.bmm(q, k) * (hd**-0.5), dim=-1)
222
+ o = torch.bmm(w, v).permute(1, 0, 2).reshape(1, n, c)
223
+ return o_proj(o)
224
+
225
+
226
+ class SiLU(torch.nn.Module):
227
+ def forward(self, x):
228
+ return x * torch.sigmoid(x)
229
+
230
+
231
+ class FoldedBatchNorm(torch.nn.Module):
232
+ """x * scale + shift with the frozen-BN scale/shift precomputed exactly as the module does."""
233
+
234
+ def __init__(self, weight, bias, mean, var, eps):
235
+ super().__init__()
236
+ scale = weight * (var + eps).rsqrt()
237
+ self.register_buffer("scale", scale.reshape(1, -1, 1, 1))
238
+ self.register_buffer("shift", (bias - mean * scale).reshape(1, -1, 1, 1))
239
+
240
+ def forward(self, x):
241
+ return x * self.scale + self.shift
242
+
243
+
244
+ def fold_frozen_bn_into_conv(root, bn_class):
245
+ """For every module with `.convolution` + frozen-BN `.normalization`, fold the BN into the conv.
246
+
247
+ Used where a conv output is too large for the runtime's 1D elementwise dispatch limit
248
+ (~4.2M elements): folded, the following ReLU fuses into et_vk.conv_with_clamp.
249
+ """
250
+ n = 0
251
+ for m in root.modules():
252
+ conv, bn = getattr(m, "convolution", None), getattr(m, "normalization", None)
253
+ if isinstance(conv, torch.nn.Conv2d) and isinstance(bn, bn_class):
254
+ scale = bn.weight * (bn.running_var + 1e-5).rsqrt()
255
+ shift = bn.bias - bn.running_mean * scale
256
+ with torch.no_grad():
257
+ conv.weight.mul_(scale.view(-1, 1, 1, 1))
258
+ bias = shift if conv.bias is None else conv.bias * scale + shift
259
+ conv.bias = torch.nn.Parameter(bias.clone())
260
+ m.normalization = torch.nn.Identity()
261
+ n += 1
262
+ return n
263
+
264
+
265
+ def relabel_golden(out_dir, note):
266
+ """_common.export_vision labels its reference case as the internal goldens; here it is upstream eager."""
267
+ import json
268
+ from pathlib import Path
269
+
270
+ p = Path(out_dir) / "validation.json"
271
+ v = json.loads(p.read_text())
272
+ if "golden" in v["cases"]:
273
+ v["cases"]["golden"]["golden_note"] = note
274
+ p.write_text(json.dumps(v, indent=2) + "\n")
275
+
276
+
277
+ def replace_modules(root, predicate, factory):
278
+ n = 0
279
+ for parent in list(root.modules()):
280
+ for name, child in list(parent.named_children()):
281
+ if predicate(child):
282
+ setattr(parent, name, factory(child))
283
+ n += 1
284
+ return n
285
+
286
+
287
+ def yolo_layout_buf(boxes_cxcywh, scores, pix_scale):
288
+ """Like yolo_layout, with the [w, h, w, h] scale passed in as a registered buffer."""
289
+ return torch.cat([boxes_cxcywh * pix_scale, scores], dim=-1).permute(0, 2, 1)
290
+
291
+
292
+ def yolo_layout(boxes_cxcywh, scores, size):
293
+ """[1, Q, 4] normalized boxes + [1, Q, C] probabilities -> [1, 4 + C, Q] in input pixels.
294
+
295
+ The layout the playground's YOLO decoder (src/vision/detect.js) already reads.
296
+ """
297
+ w, h = size
298
+ scale = torch.tensor([w, h, w, h], dtype=torch.float32)
299
+ return torch.cat([boxes_cxcywh * scale, scores], dim=-1).permute(0, 2, 1)
export/detector-dfine-small.py ADDED
@@ -0,0 +1,424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """D-FINE small (COCO, Apache-2.0) -> ExecuTorch WebGPU, for the playground's current forward runtime.
3
+
4
+ Upstream: HF transformers port `ustc-community/dfine-small-coco` (Peterande/D-FINE, D-FINE-S). Every tensor in
5
+ it is bit-identical to the authors' `dfine_s_coco.pth` on `Peterande/D-FINE` (see verify_official.py).
6
+
7
+ Input: [1, 3, 640, 640] float32 RGB in [0, 1] (image / 255; the HF processor does no mean/std).
8
+ Upstream resizes to 640x640 without keeping aspect; a letterboxed input also works.
9
+ Output: [1, 84, 300] float32, the YOLO layout src/vision/detect.js reads: rows 0-3 are
10
+ cx, cy, w, h in input pixels, rows 4-83 are sigmoid class probabilities (80 COCO classes,
11
+ contiguous ids, same order as the YOLO head). 300 queries; no NMS needed (DETR).
12
+
13
+ The eager graph is rebuilt from the HF modules with output-preserving rewrites so that the
14
+ whole program is one delegate made of operators the current runtime registers
15
+ (see _detector.py): topk -> TopKSet (same query set; queries come out in anchor order, and the
16
+ decoder is permutation equivariant), grid_sample -> gathers, inverse_sigmoid/log -> identity,
17
+ LQE top-4 -> rank-based selection, max over classes -> exact where-tree, SiLU -> x * sigmoid(x),
18
+ frozen BN -> precomputed scale/shift, position embedding and anchors -> constants.
19
+ """
20
+
21
+ import argparse
22
+ import copy
23
+ import json
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ import numpy as np
28
+ import torch
29
+ import torch.nn.functional as F
30
+
31
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
32
+ import _common # noqa: E402
33
+ import _detector as D # noqa: E402
34
+
35
+ UPSTREAM = {
36
+ "repo": "ustc-community/dfine-small-coco",
37
+ "revision": "f79e65b5fbb33ceb9d3ebba042955d7410c608f8",
38
+ "files": {
39
+ "model.safetensors": "a144baff3a27bfd538fe37b11fa8d4fca5744b8c054b5301e7c699a1379cd18f",
40
+ },
41
+ "license": "Apache-2.0",
42
+ }
43
+ OFFICIAL = {
44
+ "repo": "Peterande/D-FINE",
45
+ "revision": "19214e91f5ce4c1e609022c38b1fc953c47404e7",
46
+ "file": "dfine_s_coco.pth",
47
+ "sha256": "48a6c8cc43eb57186843f752e2e8461ddd3326e0d3c575e71e6e960844683e89",
48
+ "result": "all 764 tensors bit-identical (verify_official.py)",
49
+ }
50
+ SIZE = 640
51
+
52
+
53
+ def load_hf():
54
+ from transformers import DFineForObjectDetection
55
+
56
+ path, digest = _common.hf_file(
57
+ UPSTREAM["repo"],
58
+ "model.safetensors",
59
+ UPSTREAM["revision"],
60
+ UPSTREAM["files"]["model.safetensors"],
61
+ )
62
+ m = DFineForObjectDetection.from_pretrained(
63
+ UPSTREAM["repo"], revision=UPSTREAM["revision"], attn_implementation="eager"
64
+ ).eval()
65
+ return m, digest
66
+
67
+
68
+ class DFineStatic(torch.nn.Module):
69
+ def __init__(self, hf, size=SIZE):
70
+ super().__init__()
71
+ cfg = hf.config
72
+ self.cfg = cfg
73
+ mdl = hf.model
74
+ self.backbone = mdl.backbone.model
75
+ from transformers.models.d_fine.modeling_d_fine import DFineFrozenBatchNorm2d
76
+
77
+ D.replace_modules(
78
+ self.backbone,
79
+ lambda c: isinstance(c, DFineFrozenBatchNorm2d),
80
+ lambda c: D.FoldedBatchNorm(
81
+ c.weight, c.bias, c.running_mean, c.running_var, 1e-5
82
+ ),
83
+ )
84
+ self.encoder_input_proj = mdl.encoder_input_proj
85
+ self.encoder = mdl.encoder
86
+ D.replace_modules(
87
+ self,
88
+ lambda c: type(c).__name__ in ("SiLUActivation", "SiLU"),
89
+ lambda c: D.SiLU(),
90
+ )
91
+ self.decoder_input_proj = mdl.decoder_input_proj
92
+ self.enc_output = mdl.enc_output
93
+ self.enc_score_head = mdl.enc_score_head
94
+ self.enc_bbox_head = mdl.enc_bbox_head
95
+ self.decoder = mdl.decoder
96
+ self.class_embed = hf.class_embed
97
+ self.bbox_embed = hf.bbox_embed
98
+
99
+ strides = cfg.feat_strides
100
+ self.shapes = [(size // s, size // s) for s in strides]
101
+ for _ in range(cfg.num_feature_levels - len(strides)):
102
+ h, w = self.shapes[-1]
103
+ self.shapes.append(((h + 1) // 2, (w + 1) // 2))
104
+ n = sum(h * w for h, w in self.shapes)
105
+ anchors, valid = mdl.generate_anchors(
106
+ tuple(self.shapes), device="cpu", dtype=torch.float32
107
+ )
108
+ self.register_buffer("anchors", anchors.clone(), persistent=False)
109
+ self.register_buffer("valid", valid.to(torch.float32).clone(), persistent=False)
110
+ aifi_hw = self.shapes[cfg.encode_proj_layers[0]]
111
+ from transformers.models.d_fine.modeling_d_fine import (
112
+ build_2d_sinusoidal_position_embedding,
113
+ )
114
+
115
+ pos = build_2d_sinusoidal_position_embedding(
116
+ aifi_hw[0],
117
+ aifi_hw[1],
118
+ cfg.encoder_hidden_dim,
119
+ cfg.positional_encoding_temperature,
120
+ )
121
+ self.register_buffer(
122
+ "aifi_pos", pos.unsqueeze(0).to(torch.float32), persistent=False
123
+ )
124
+ from transformers.models.d_fine.modeling_d_fine import weighting_function
125
+
126
+ with torch.no_grad():
127
+ project = weighting_function(
128
+ cfg.max_num_bins, self.decoder.up, self.decoder.reg_scale
129
+ )
130
+ self.register_buffer(
131
+ "project", project.reshape(1, -1).clone(), persistent=False
132
+ )
133
+ self.reg_scale = float(self.decoder.reg_scale.item())
134
+ self.topk = D.TopKSet(n, cfg.num_queries)
135
+ self.sampler = D.DeformSampler(
136
+ self.shapes,
137
+ list(self.decoder.layers[0].encoder_attn.num_points_list),
138
+ cfg.decoder_attention_heads,
139
+ cfg.d_model // cfg.decoder_attention_heads,
140
+ )
141
+ self.register_buffer(
142
+ "pix_scale",
143
+ torch.tensor([size, size, size, size], dtype=torch.float32),
144
+ persistent=False,
145
+ )
146
+ self.register_buffer("one", torch.ones(1), persistent=False)
147
+ self.register_buffer("two", torch.full((1,), 2.0), persistent=False)
148
+ self.register_buffer("zero", torch.zeros(1), persistent=False)
149
+
150
+ # ---- encoder -------------------------------------------------------------------------
151
+ def aifi(self, x):
152
+ cfg = self.cfg
153
+ b, c, h, w = x.shape
154
+ hs = x.flatten(2).permute(0, 2, 1)
155
+ for layer in self.encoder.aifi[0].layers:
156
+ a = layer.self_attn
157
+ att = D.attention(
158
+ a.q_proj,
159
+ a.k_proj,
160
+ a.v_proj,
161
+ a.o_proj,
162
+ hs,
163
+ self.aifi_pos,
164
+ cfg.encoder_attention_heads,
165
+ )
166
+ hs = layer.self_attn_layer_norm(hs + att)
167
+ hs = layer.final_layer_norm(hs + layer.mlp(hs))
168
+ return hs.permute(0, 2, 1).reshape(b, c, h, w)
169
+
170
+ def run_encoder(self, feats):
171
+ enc = self.encoder
172
+ feats = list(feats)
173
+ idx = self.cfg.encode_proj_layers[0]
174
+ feats[idx] = self.aifi(feats[idx])
175
+ fpn = [feats[-1]]
176
+ for i, (lat, blk) in enumerate(zip(enc.lateral_convs, enc.fpn_blocks)):
177
+ back = feats[enc.num_fpn_stages - i - 1]
178
+ top = lat(fpn[-1])
179
+ fpn[-1] = top
180
+ top = F.interpolate(top, scale_factor=2.0, mode="nearest")
181
+ fpn.append(blk(torch.cat([top, back], dim=1)))
182
+ fpn.reverse()
183
+ pan = [fpn[0]]
184
+ for i, (down, blk) in enumerate(zip(enc.downsample_convs, enc.pan_blocks)):
185
+ pan.append(blk(torch.cat([down(pan[-1]), fpn[i + 1]], dim=1)))
186
+ return pan
187
+
188
+ # ---- decoder -------------------------------------------------------------------------
189
+ def deform(self, attn_mod, hidden, ref, value):
190
+ """hidden [1, Q, C]; ref [1, Q, 4]; value [L, heads, hd]."""
191
+ cfg = self.cfg
192
+ q = hidden.shape[1]
193
+ nh = cfg.decoder_attention_heads
194
+ P = sum(attn_mod.num_points_list)
195
+ off = attn_mod.sampling_offsets(hidden).view(q, nh, P, 2)
196
+ aw = torch.softmax(attn_mod.attention_weights(hidden).view(q, nh, P), dim=-1)
197
+ scale = attn_mod.num_points_scale.view(1, 1, P, 1)
198
+ r = ref.view(q, 1, 1, 4)
199
+ loc = r[..., :2] + off * scale * r[..., 2:] * attn_mod.offset_scale
200
+ out = self.sampler(value, loc, aw).view(1, q, cfg.d_model)
201
+ return out
202
+
203
+ def decoder_layer(self, layer, hs, pos, ref, value):
204
+ cfg = self.cfg
205
+ a = layer.self_attn
206
+ att = D.attention(
207
+ a.q_proj, a.k_proj, a.v_proj, a.o_proj, hs, pos, cfg.decoder_attention_heads
208
+ )
209
+ hs = layer.self_attn_layer_norm(hs + att)
210
+ cross = self.deform(layer.encoder_attn, hs + pos, ref, value)
211
+ hs = layer.gateway(hs, cross)
212
+ hs = hs + layer.mlp(hs)
213
+ return layer.final_layer_norm(D.clamp(hs, -65504.0, 65504.0))
214
+
215
+ def integral(self, corners):
216
+ q = corners.shape[1]
217
+ p = torch.softmax(corners.reshape(q * 4, self.cfg.max_num_bins + 1), dim=-1)
218
+ return F.linear(p, self.project).reshape(1, q, 4)
219
+
220
+ def distance2bbox(self, pts, dist):
221
+ rs = self.reg_scale
222
+ cx, cy, w, h = pts[..., 0:1], pts[..., 1:2], pts[..., 2:3], pts[..., 3:4]
223
+ x0 = cx - (0.5 * rs + dist[..., 0:1]) * (w / rs)
224
+ y0 = cy - (0.5 * rs + dist[..., 1:2]) * (h / rs)
225
+ x1 = cx + (0.5 * rs + dist[..., 2:3]) * (w / rs)
226
+ y1 = cy + (0.5 * rs + dist[..., 3:4]) * (h / rs)
227
+ return torch.cat([(x0 + x1) / 2, (y0 + y1) / 2, x1 - x0, y1 - y0], dim=-1)
228
+
229
+ def lqe(self, lqe_mod, scores, corners):
230
+ q = corners.shape[1]
231
+ nb = self.cfg.max_num_bins + 1
232
+ k = self.cfg.top_prob_values
233
+ prob = torch.softmax(corners.reshape(q * 4, nb), dim=-1) # [R, nb]
234
+ diff = prob.view(q * 4, 1, nb) - prob.view(
235
+ q * 4, nb, 1
236
+ ) # [R, j, l] = p_l - p_j
237
+ greater = torch.where(diff > 0, self.one, self.zero).sum(dim=-1) # [R, nb]
238
+ tops = []
239
+ for i in range(k):
240
+ masked = torch.where(greater <= i, prob, self.two)
241
+ tops.append(D.tree_reduce(masked, D.where_min)) # i-th largest value
242
+ top = torch.cat(tops, dim=-1)
243
+ stat = torch.cat([top, top.mean(dim=-1, keepdim=True)], dim=-1).reshape(
244
+ 1, q, 4 * (k + 1)
245
+ )
246
+ return scores + lqe_mod.reg_conf(stat)
247
+
248
+ def forward(self, pixel_values):
249
+ cfg = self.cfg
250
+ feats = self.backbone(pixel_values).feature_maps
251
+ proj = [self.encoder_input_proj[i](f) for i, f in enumerate(feats)]
252
+ enc = self.run_encoder(proj)
253
+ sources = [self.decoder_input_proj[i](s) for i, s in enumerate(enc)]
254
+ for i in range(len(sources), cfg.num_feature_levels):
255
+ sources.append(self.decoder_input_proj[i](enc[-1]))
256
+ flat = torch.cat(
257
+ [s.flatten(2).permute(0, 2, 1) for s in sources], dim=1
258
+ ) # [1, L, C]
259
+ L = flat.shape[1]
260
+
261
+ memory = self.valid * flat
262
+ out_mem = self.enc_output(memory)
263
+ cls = self.enc_score_head(out_mem) # [1, L, 80]
264
+ coord = self.enc_bbox_head(out_mem) + self.anchors # [1, L, 4]
265
+ best = D.tree_reduce(cls, D.where_max).view(1, L)
266
+ idx = self.topk(best) # [Q] float, ascending anchor order
267
+ Q = cfg.num_queries
268
+ idx_c = idx.view(1, Q, 1).expand(1, Q, cfg.d_model).to(torch.int64)
269
+ target = torch.gather(out_mem, 1, idx_c)
270
+ idx_4 = idx.view(1, Q, 1).expand(1, Q, 4).to(torch.int64)
271
+ ref_unact = torch.gather(coord, 1, idx_4)
272
+
273
+ nh = cfg.decoder_attention_heads
274
+ value = flat.view(L, nh, cfg.d_model // nh)
275
+ dec = self.decoder
276
+ hs = target
277
+ ref = torch.sigmoid(ref_unact)
278
+ out_detach = None
279
+ corners_prev = None
280
+ ref_initial = None
281
+ for i, layer in enumerate(dec.layers):
282
+ pos = D.clamp(dec.query_pos_head(ref), -10.0, 10.0)
283
+ hs = self.decoder_layer(layer, hs, pos, ref, value)
284
+ if i == 0:
285
+ ref_initial = D.sigmoid_add_logit(dec.pre_bbox_head(hs), ref)
286
+ if out_detach is None:
287
+ corners = self.bbox_embed[i](hs)
288
+ else:
289
+ corners = self.bbox_embed[i](hs + out_detach) + corners_prev
290
+ box = self.distance2bbox(ref_initial, self.integral(corners))
291
+ corners_prev = corners
292
+ ref = box
293
+ out_detach = hs
294
+ last = len(dec.layers) - 1
295
+ scores = self.class_embed[last](hs)
296
+ scores = self.lqe(dec.lqe_layers[last], scores, corners)
297
+ return D.yolo_layout_buf(box, torch.sigmoid(scores), self.pix_scale)
298
+
299
+
300
+ # ---------------------------------------------------------------------------------------------
301
+ def hf_reference(hf, x):
302
+ """HF eager outputs in the same layout, with queries permuted to ascending anchor order."""
303
+ with torch.no_grad():
304
+ o = hf(pixel_values=x)
305
+ order = torch.topk(
306
+ o.enc_outputs_class.max(-1).values, hf.config.num_queries, dim=1
307
+ ).indices[0]
308
+ perm = torch.argsort(order)
309
+ boxes = o.pred_boxes[:, perm]
310
+ probs = torch.sigmoid(o.logits[:, perm])
311
+ return torch.cat([boxes * SIZE, probs], dim=-1).permute(0, 2, 1).numpy(), order
312
+
313
+
314
+ def load_image(path, size=SIZE):
315
+ from PIL import Image
316
+
317
+ img = Image.open(path).convert("RGB").resize((size, size), Image.BILINEAR)
318
+ a = np.asarray(img, dtype=np.float32) / 255.0
319
+ return torch.from_numpy(a.transpose(2, 0, 1).copy()).unsqueeze(0)
320
+
321
+
322
+ def top_detections(out, thr=0.4):
323
+ out = np.asarray(out)[0]
324
+ probs = out[4:]
325
+ cls = probs.argmax(0)
326
+ sc = probs.max(0)
327
+ keep = np.argsort(-sc)
328
+ res = []
329
+ for q in keep:
330
+ if sc[q] < thr:
331
+ break
332
+ cx, cy, w, h = out[:4, q]
333
+ res.append(
334
+ (
335
+ int(cls[q]),
336
+ round(float(sc[q]), 3),
337
+ [
338
+ round(float(v), 1)
339
+ for v in (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2)
340
+ ],
341
+ )
342
+ )
343
+ return res
344
+
345
+
346
+ def main():
347
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
348
+ ap.add_argument("--out", required=True)
349
+ ap.add_argument(
350
+ "--image",
351
+ required=True,
352
+ help="any RGB photo; the rewritten graph is checked against upstream eager on it",
353
+ )
354
+ args = ap.parse_args()
355
+ out = Path(args.out)
356
+ out.mkdir(parents=True, exist_ok=True)
357
+
358
+ hf, digest = load_hf()
359
+ model = DFineStatic(copy.deepcopy(hf)).eval()
360
+ img = load_image(args.image)
361
+
362
+ # Output-preservation check of the rewritten eager graph against HF eager, real image.
363
+ ref, order = hf_reference(hf, img)
364
+ with torch.no_grad():
365
+ mine = model(img).numpy()
366
+ parity = {
367
+ "image": Path(args.image).name,
368
+ "rewritten_eager_vs_hf_eager": _common.compare(mine, ref),
369
+ }
370
+ parity["rewritten_eager_vs_hf_eager"]["max_abs_boxes_px"] = float(
371
+ np.abs(mine[:, :4] - ref[:, :4]).max()
372
+ )
373
+ parity["rewritten_eager_vs_hf_eager"]["max_abs_probs"] = float(
374
+ np.abs(mine[:, 4:] - ref[:, 4:]).max()
375
+ )
376
+ parity["detections_hf"] = top_detections(ref)
377
+ parity["detections_rewritten"] = top_detections(mine)
378
+ print(json.dumps(parity, indent=1))
379
+
380
+ ep, webgpu, portable = _common.export_vision(
381
+ name="dfine_s_coco",
382
+ model=model,
383
+ example_input=img,
384
+ out_dir=out,
385
+ upstream={
386
+ "model": "D-FINE-S (COCO), HF transformers port",
387
+ "weights": {
388
+ "repo": UPSTREAM["repo"],
389
+ "revision": UPSTREAM["revision"],
390
+ "file": "model.safetensors",
391
+ "sha256": digest,
392
+ "verified_against": OFFICIAL,
393
+ },
394
+ "license": UPSTREAM["license"],
395
+ },
396
+ extra_recipe={
397
+ "preprocessing_outside_graph": "RGB resize to 640x640 (letterbox also works), / 255; no mean/std",
398
+ "postprocessing_outside_graph": "output [1, 84, 300]: cx, cy, w, h (input px) + 80 sigmoid probs; threshold; NMS optional",
399
+ "rewrites": "see recipes/detector-dfine-small.py docstring and recipes/_detector.py",
400
+ },
401
+ golden=(img.numpy(), ref),
402
+ )
403
+ # Real-image check of the exported paths as well.
404
+ D.relabel_golden(
405
+ out,
406
+ "internal_golden_* here = upstream HF transformers eager output on the real image (same layout; DETR queries permuted to anchor order)",
407
+ )
408
+ port = _common.run_pte(portable.buffer, (img,))[0]
409
+ parity["portable_pte_vs_hf_eager"] = _common.compare(port, ref)
410
+ parity["portable_pte_vs_rewritten_eager"] = _common.compare(port, mine)
411
+ parity["detections_portable_pte"] = top_detections(port)
412
+ (out / "hf_parity.json").write_text(json.dumps(parity, indent=1) + "\n")
413
+ np.save(out / "cpu_eager_out.npy", mine)
414
+ img.numpy().astype(np.float32).tofile(out / "input_640.f32")
415
+ print(
416
+ json.dumps(
417
+ {k: v for k, v in parity.items() if not k.startswith("detections")},
418
+ indent=1,
419
+ )
420
+ )
421
+
422
+
423
+ if __name__ == "__main__":
424
+ main()
export/recipe.json ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "dfine_s_coco",
3
+ "upstream": {
4
+ "model": "D-FINE-S (COCO), HF transformers port",
5
+ "weights": {
6
+ "repo": "ustc-community/dfine-small-coco",
7
+ "revision": "f79e65b5fbb33ceb9d3ebba042955d7410c608f8",
8
+ "file": "model.safetensors",
9
+ "sha256": "a144baff3a27bfd538fe37b11fa8d4fca5744b8c054b5301e7c699a1379cd18f",
10
+ "verified_against": {
11
+ "repo": "Peterande/D-FINE",
12
+ "revision": "19214e91f5ce4c1e609022c38b1fc953c47404e7",
13
+ "file": "dfine_s_coco.pth",
14
+ "sha256": "48a6c8cc43eb57186843f752e2e8461ddd3326e0d3c575e71e6e960844683e89",
15
+ "result": "all 764 tensors bit-identical (verify_official.py)"
16
+ }
17
+ },
18
+ "license": "Apache-2.0"
19
+ },
20
+ "versions": {
21
+ "python": "3.12.15+meta",
22
+ "torch": "2.14.1+cpu",
23
+ "torchao": "0.18.0+cpu",
24
+ "executorch": "1.5.1+cpu",
25
+ "executorch_git": "3b60683923245cf472b7323426920e15623ba361",
26
+ "transformers": "5.19.0",
27
+ "huggingface_hub": "2.2.0"
28
+ },
29
+ "export": {
30
+ "api": "torch.export.export(strict=False) -> to_edge_transform_and_lower(partitioner=[WebGPUPartitioner()]) -> to_executorch()",
31
+ "partitioner": "executorch.backends.webgpu.partitioner.WebGPUPartitioner() (default compile options; wraps VulkanPartitioner)",
32
+ "inputs": [
33
+ {
34
+ "shape": [
35
+ 1,
36
+ 3,
37
+ 640,
38
+ 640
39
+ ],
40
+ "dtype": "float32",
41
+ "layout": "NCHW"
42
+ }
43
+ ],
44
+ "outputs": [
45
+ {
46
+ "shape": [
47
+ 1,
48
+ 84,
49
+ 300
50
+ ],
51
+ "dtype": "float32"
52
+ }
53
+ ],
54
+ "dtype": "float32",
55
+ "quantization": "none (fp32)",
56
+ "preprocessing_outside_graph": "RGB resize to 640x640 (letterbox also works), / 255; no mean/std",
57
+ "postprocessing_outside_graph": "output [1, 84, 300]: cx, cy, w, h (input px) + 80 sigmoid probs; threshold; NMS optional",
58
+ "rewrites": "see recipes/detector-dfine-small.py docstring and recipes/_detector.py"
59
+ },
60
+ "files": {
61
+ "dfine_s_coco_webgpu.pte": {
62
+ "bytes": 42260612,
63
+ "sha256": "96cc65f615dc2c79d1da80ef075b831fac8e39e8dcb58af9aff5bab154a4c4d6"
64
+ },
65
+ "dfine_s_coco_portable.pte": {
66
+ "bytes": 42299640,
67
+ "sha256": "f36da046a83ee72fe4083e61253e6ae5dc52d843ba38faaf73fb10ecd29da8ac",
68
+ "note": "CPU-only validation artifact (portable kernels, no delegate); not for release"
69
+ }
70
+ },
71
+ "graph": {
72
+ "non_delegated_kernels": {},
73
+ "delegates": [
74
+ {
75
+ "id": "VulkanBackend",
76
+ "ops": {
77
+ "et_vk.prepack.default": 603,
78
+ "aten.convolution.default": 99,
79
+ "aten.mul.Tensor": 281,
80
+ "aten.add.Tensor": 254,
81
+ "aten.relu.default": 48,
82
+ "aten.constant_pad_nd.default": 2,
83
+ "aten.max_pool2d_with_indices.default": 1,
84
+ "aten.cat.default": 66,
85
+ "aten.clone.default": 5,
86
+ "aten.view_copy.default": 110,
87
+ "aten.permute_copy.default": 37,
88
+ "aten.expand_copy.default": 8,
89
+ "aten.bmm.default": 11,
90
+ "aten._softmax.default": 11,
91
+ "aten.linear.default": 59,
92
+ "aten.native_layer_norm.default": 12,
93
+ "aten.squeeze_copy.dims": 33,
94
+ "aten.gelu.default": 1,
95
+ "aten.upsample_nearest2d.vec": 2,
96
+ "aten.sigmoid.default": 47,
97
+ "aten.split_with_sizes_copy.default": 7,
98
+ "aten.slice_copy.Tensor": 190,
99
+ "aten.sub.Tensor": 358,
100
+ "aten.gt.Scalar": 71,
101
+ "aten.where.self": 308,
102
+ "aten.lt.Scalar": 89,
103
+ "aten.ge.Scalar": 97,
104
+ "aten.sum.dim_IntList": 56,
105
+ "dim_order_ops._to_dim_order_copy.default": 71,
106
+ "aten.unsqueeze_copy.default": 6,
107
+ "aten.gather.default": 5,
108
+ "aten.div.Tensor": 25,
109
+ "aten.neg.default": 1,
110
+ "aten.le.Scalar": 4,
111
+ "aten.mean.dim": 1
112
+ },
113
+ "inputs": 1,
114
+ "outputs": 1
115
+ }
116
+ ]
117
+ },
118
+ "command": "python recipes/detector-dfine-small.py --out out/detector-dfine-small --image photo.jpg"
119
+ }
export/requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment the re-exports in REEXPORT.md were produced with (Python 3.12, Linux x86_64, CPU only).
2
+ # python -m venv venv
3
+ # venv/bin/pip install -r recipes/requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
4
+ # executorch 1.5.1 = pytorch/executorch git 3b60683923245cf472b7323426920e15623ba361; its wheel ships
5
+ # backends/webgpu (WebGPUPartitioner) and examples/models/llama (export_llm).
6
+ executorch==1.5.1
7
+ torch==2.14.1+cpu
8
+ torchvision==0.29.1+cpu
9
+ torchao==0.18.0+cpu
10
+ transformers==5.19.0
11
+ huggingface_hub==2.2.0
12
+ safetensors==0.8.0
13
+ tokenizers==0.23.3
14
+ numpy==2.5.4
15
+ pillow==12.3.0
export/verify_official.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Check that every tensor in a converted Hugging Face D-FINE checkpoint is bit-identical to a
2
+ tensor (or a q/k/v third of an in_proj tensor) of the official checkpoint.
3
+
4
+ python verify_official.py Peterande/D-FINE 19214e91f5ce4c1e609022c38b1fc953c47404e7 dfine_s_coco.pth \
5
+ ustc-community/dfine-small-coco f79e65b5fbb33ceb9d3ebba042955d7410c608f8
6
+ """
7
+ import hashlib
8
+ import json
9
+ import sys
10
+
11
+ import torch
12
+ from huggingface_hub import hf_hub_download
13
+ from safetensors.torch import load_file
14
+
15
+ off_repo, off_rev, off_file, port_repo, port_rev = sys.argv[1:6]
16
+
17
+
18
+ def digest(t):
19
+ t = t.detach().contiguous().cpu()
20
+ return hashlib.sha256(str(t.dtype).encode() + str(tuple(t.shape)).encode() + t.numpy().tobytes()).hexdigest()
21
+
22
+
23
+ off_path = hf_hub_download(off_repo, off_file, revision=off_rev)
24
+ ckpt = torch.load(off_path, map_location="cpu", weights_only=False)
25
+ official = ckpt["model"]
26
+ pool = {}
27
+ for k, t in official.items():
28
+ if not torch.is_tensor(t):
29
+ continue
30
+ pool.setdefault(digest(t), k)
31
+ if "in_proj" in k and t.shape[0] % 3 == 0:
32
+ for i, part in enumerate(t.chunk(3, dim=0)):
33
+ pool.setdefault(digest(part), f"{k}[{'qkv'[i]}]")
34
+
35
+ port_path = hf_hub_download(port_repo, "model.safetensors", revision=port_rev)
36
+ port = load_file(port_path)
37
+ matched, unmatched = 0, []
38
+ for k, t in port.items():
39
+ if digest(t) in pool:
40
+ matched += 1
41
+ else:
42
+ unmatched.append((k, tuple(t.shape)))
43
+ nparams = sum(t.numel() for t in port.values())
44
+ unparams = sum(port[k].numel() for k, _ in unmatched)
45
+ print(json.dumps({
46
+ "official": f"{off_repo}@{off_rev}/{off_file}",
47
+ "official_sha256": hashlib.sha256(open(off_path, "rb").read()).hexdigest(),
48
+ "official_keys": list(ckpt.keys()),
49
+ "port": f"{port_repo}@{port_rev}/model.safetensors",
50
+ "port_tensors": len(port),
51
+ "matched": matched,
52
+ "unmatched": len(unmatched),
53
+ "unmatched_params": unparams,
54
+ "total_params": nparams,
55
+ "unmatched_sample": unmatched[:25],
56
+ }, indent=1))
manifest.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "dfine-small",
3
+ "repo": "executorch-community/D-FINE-Small-COCO-ExecuTorch-WebGPU",
4
+ "files": [
5
+ {
6
+ "path": "dfine_s_coco_webgpu.pte",
7
+ "bytes": 42260612,
8
+ "sha256": "96cc65f615dc2c79d1da80ef075b831fac8e39e8dcb58af9aff5bab154a4c4d6"
9
+ }
10
+ ]
11
+ }