Instructions to use wonfy/BiRefNet-ONNX-webgpu with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers.js
How to use wonfy/BiRefNet-ONNX-webgpu with Transformers.js:
// npm i @huggingface/transformers import { pipeline } from '@huggingface/transformers'; // Allocate pipeline const pipe = await pipeline('image-segmentation', 'wonfy/BiRefNet-ONNX-webgpu');
BiRefNet rebuilt for WebGPU, fp32 and fp16
Browse files- LICENSE +21 -0
- README.md +94 -0
- config.json +6 -0
- onnx/model.onnx +3 -0
- onnx/model_fp16.onnx +3 -0
- preprocessor_config.json +12 -0
- scripts/fold.py +12 -0
- scripts/patch.py +197 -0
- scripts/to_fp16.py +14 -0
- scripts/verify.py +52 -0
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 ZhengPeng
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: transformers.js
|
| 4 |
+
base_model: ZhengPeng7/BiRefNet
|
| 5 |
+
pipeline_tag: image-segmentation
|
| 6 |
+
tags:
|
| 7 |
+
- background-removal
|
| 8 |
+
- onnx
|
| 9 |
+
- webgpu
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# BiRefNet, ONNX for WebGPU
|
| 13 |
+
|
| 14 |
+
[BiRefNet](https://huggingface.co/ZhengPeng7/BiRefNet) for background removal, rebuilt so it runs on WebGPU in the browser. This is the full model with the Swin-L backbone. For the smaller one see [wonfy/BiRefNet_lite-ONNX-webgpu](https://huggingface.co/wonfy/BiRefNet_lite-ONNX-webgpu). The weights are unchanged. Only the graph was rewritten, starting from [onnx-community/BiRefNet-ONNX](https://huggingface.co/onnx-community/BiRefNet-ONNX).
|
| 15 |
+
|
| 16 |
+
The stock export doesn't run on WebGPU. Some of its shaders need more storage buffers than a GPU allows (WebGPU only promises 8 per shader), and its int64 index maths and `Sum` nodes have no WebGPU kernels, so they fall back to the CPU with very large tensors.
|
| 17 |
+
|
| 18 |
+
| File | Precision | Size |
|
| 19 |
+
|---|---|---|
|
| 20 |
+
| `onnx/model.onnx` | fp32 | 1038 MB |
|
| 21 |
+
| `onnx/model_fp16.onnx` | fp16 weights, float32 input and output | 520 MB |
|
| 22 |
+
|
| 23 |
+
Input `input_image`: float32 `[1, 3, 1024, 1024]`, RGB resized to 1024 x 1024, scaled to 0..1, then ImageNet mean `[0.485, 0.456, 0.406]` and std `[0.229, 0.224, 0.225]`.
|
| 24 |
+
Output `output_image`: float32 `[1, 1, 1024, 1024]` logits. Apply a sigmoid to get the alpha mask.
|
| 25 |
+
|
| 26 |
+
## What was changed
|
| 27 |
+
|
| 28 |
+
1. **Constant folding** with ONNX Runtime's basic optimizer, which only produces standard ONNX ops. It takes the graph from 21,968 to 5,271 nodes and removes every shape op, `ScatterND`, `Where` and `ConstantOfShape`.
|
| 29 |
+
2. **4 `Concat` nodes** with 16, 64, 256 and 1024 inputs became trees with at most 6 inputs each.
|
| 30 |
+
3. **59 `Split` nodes** with up to 32 outputs became trees with at most 6 outputs each.
|
| 31 |
+
4. **20 `Sum` nodes** became chains of `Add` in the same order, so the result stays bit-identical.
|
| 32 |
+
5. **80 `GatherND` nodes** (the sampling in the deformable convolutions) became a plain `Gather` on the data reshaped to `[H*W, C]`. Their index maths, which was int64, now runs in float (the values are whole numbers), then `y * W + x` is taken in int32. No int64 tensor is left in the graph. Doing the last step in int32 also keeps the index exact in the fp16 model.
|
| 33 |
+
6. **fp16:** converted with `onnxconverter-common` and `keep_io_types`, so input and output stay float32.
|
| 34 |
+
|
| 35 |
+
Every node now uses at most 7 storage buffers, under the WebGPU minimum of 8.
|
| 36 |
+
|
| 37 |
+
## Results
|
| 38 |
+
|
| 39 |
+
On the CPU (ONNX Runtime 1.31, three test images: hair, a car and two cats), compared with the original onnx-community fp32 model:
|
| 40 |
+
|
| 41 |
+
| File | Logits | Alpha mask |
|
| 42 |
+
|---|---|---|
|
| 43 |
+
| `model.onnx` | bit-identical, max difference 0.0 | identical |
|
| 44 |
+
| `model_fp16.onnx` | max difference 0.06 to 1.25 | mean difference under 1e-5, at most 0.02% of pixels move by more than 1/255 |
|
| 45 |
+
|
| 46 |
+
On WebGPU (Chrome 155, AMD Radeon RX 9070 XT, Transformers.js 4.3.1 with ONNX Runtime Web 1.31.0-dev.20260914), `model_fp16.onnx` on the same three inputs:
|
| 47 |
+
|
| 48 |
+
- About 800 ms per image once warm, about 4.3 s to load.
|
| 49 |
+
- 0.5% to 1.3% of pixels move by more than 1/255 from the fp32 CPU result, and 0.01% to 0.02% of pixels change between inside and outside, all of them on edges.
|
| 50 |
+
|
| 51 |
+
The fp32 file is about 1 GB. ONNX Runtime Web reads the whole model into WebAssembly memory before it moves it to the GPU, so the fp16 file is the safer choice in a browser.
|
| 52 |
+
|
| 53 |
+
## Use it
|
| 54 |
+
|
| 55 |
+
With Transformers.js:
|
| 56 |
+
|
| 57 |
+
```js
|
| 58 |
+
import { AutoModel, AutoProcessor, RawImage } from "@huggingface/transformers";
|
| 59 |
+
|
| 60 |
+
const model = await AutoModel.from_pretrained("wonfy/BiRefNet-ONNX-webgpu", {
|
| 61 |
+
device: "webgpu",
|
| 62 |
+
dtype: "fp32",
|
| 63 |
+
model_file_name: "model_fp16", // leave this out for the fp32 file
|
| 64 |
+
});
|
| 65 |
+
const processor = await AutoProcessor.from_pretrained("wonfy/BiRefNet-ONNX-webgpu");
|
| 66 |
+
|
| 67 |
+
const image = await RawImage.fromURL("photo.jpg");
|
| 68 |
+
const { pixel_values } = await processor(image);
|
| 69 |
+
const { output_image } = await model({ input_image: pixel_values });
|
| 70 |
+
// alpha = sigmoid(output_image.data[i]), at 1024 x 1024
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
`dtype: "fp32"` with `model_file_name: "model_fp16"` is the combination that was tested. It loads the fp16 file as it is.
|
| 74 |
+
|
| 75 |
+
With ONNX Runtime Web directly, create an `InferenceSession` from either file with `executionProviders: ["webgpu"]`.
|
| 76 |
+
|
| 77 |
+
## Rebuild it
|
| 78 |
+
|
| 79 |
+
The scripts are in `scripts/`. They were run with Python 3.13, onnx 1.23.2, onnxruntime 1.31.0, onnxconverter-common 1.16.0, numpy and Pillow.
|
| 80 |
+
|
| 81 |
+
```
|
| 82 |
+
python scripts/fold.py model.onnx folded.onnx
|
| 83 |
+
python scripts/patch.py folded.onnx onnx/model.onnx
|
| 84 |
+
python scripts/to_fp16.py onnx/model.onnx onnx/model_fp16.onnx
|
| 85 |
+
python scripts/verify.py samples/ results/ model.onnx onnx/model.onnx onnx/model_fp16.onnx
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
`model.onnx` in the first line is the fp32 file from onnx-community/BiRefNet-ONNX. `patch.py` prints an audit at the end: the most storage buffers any node uses, and any op still producing int64.
|
| 89 |
+
|
| 90 |
+
## Licence
|
| 91 |
+
|
| 92 |
+
MIT, the same as BiRefNet. See `LICENSE`. BiRefNet is by Peng Zheng et al., [paper](https://arxiv.org/abs/2401.03407) and [code](https://github.com/ZhengPeng7/BiRefNet).
|
| 93 |
+
|
| 94 |
+
BiRefNet's training data includes DIS5K, and DIS5K's own terms restrict commercial use of the dataset. The MIT licence covers the model and code, not the training data. Check what that means for your use.
|
config.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "swin",
|
| 3 |
+
"transformers.js_config": {
|
| 4 |
+
"dtype": "fp32"
|
| 5 |
+
}
|
| 6 |
+
}
|
onnx/model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:90a8e190dcc73f0f553f83df14aa0bf7ad888c3cfe8f2180d9ab474ca86462d4
|
| 3 |
+
size 1038412111
|
onnx/model_fp16.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f5a69905dd90e6592a8030d3f7d1701d3c8c6bb840515691b2f7aab2e3d4bfd6
|
| 3 |
+
size 520175713
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_normalize": true,
|
| 3 |
+
"do_rescale": true,
|
| 4 |
+
"do_resize": true,
|
| 5 |
+
"feature_extractor_type": "ViTFeatureExtractor",
|
| 6 |
+
"image_mean": [0.485, 0.456, 0.406],
|
| 7 |
+
"image_processor_type": "ViTFeatureExtractor",
|
| 8 |
+
"image_std": [0.229, 0.224, 0.225],
|
| 9 |
+
"resample": 2,
|
| 10 |
+
"rescale_factor": 0.00392156862745098,
|
| 11 |
+
"size": { "height": 1024, "width": 1024 }
|
| 12 |
+
}
|
scripts/fold.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
import onnxruntime as ort
|
| 5 |
+
|
| 6 |
+
source, target = sys.argv[1], sys.argv[2]
|
| 7 |
+
options = ort.SessionOptions()
|
| 8 |
+
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC
|
| 9 |
+
options.optimized_model_filepath = target
|
| 10 |
+
started = time.time()
|
| 11 |
+
ort.InferenceSession(source, options, providers=["CPUExecutionProvider"])
|
| 12 |
+
print(f"folded in {time.time() - started:.1f} s -> {target}")
|
scripts/patch.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rewrites a constant-folded BiRefNet graph so every WebGPU shader fits in 8 storage buffers and nothing falls back to the CPU."""
|
| 2 |
+
import math
|
| 3 |
+
import sys
|
| 4 |
+
from collections import Counter, defaultdict
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import onnx
|
| 8 |
+
from onnx import TensorProto, helper, numpy_helper, shape_inference
|
| 9 |
+
|
| 10 |
+
FAN = 6
|
| 11 |
+
|
| 12 |
+
source, target = sys.argv[1], sys.argv[2]
|
| 13 |
+
model = shape_inference.infer_shapes(onnx.load(source))
|
| 14 |
+
graph = model.graph
|
| 15 |
+
shapes = {}
|
| 16 |
+
for value in list(graph.value_info) + list(graph.input) + list(graph.output):
|
| 17 |
+
t = value.type.tensor_type
|
| 18 |
+
shapes[value.name] = ([d.dim_value for d in t.shape.dim], t.elem_type)
|
| 19 |
+
inits = {i.name: i for i in graph.initializer}
|
| 20 |
+
for name, init in inits.items():
|
| 21 |
+
shapes[name] = (list(init.dims), init.data_type)
|
| 22 |
+
|
| 23 |
+
counter = Counter()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def fresh(base):
|
| 27 |
+
counter[base] += 1
|
| 28 |
+
return f"wonfy/{base}/{counter[base]}"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
new_inits = []
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def const(base, array):
|
| 35 |
+
name = fresh(base)
|
| 36 |
+
new_inits.append(numpy_helper.from_array(np.asarray(array), name))
|
| 37 |
+
return name
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def attr(node, name, default=None):
|
| 41 |
+
for a in node.attribute:
|
| 42 |
+
if a.name == name:
|
| 43 |
+
return helper.get_attribute_value(a)
|
| 44 |
+
return default
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
consumers = defaultdict(list)
|
| 48 |
+
for n in graph.node:
|
| 49 |
+
for x in n.input:
|
| 50 |
+
consumers[x].append(n)
|
| 51 |
+
producers = {out: n for n in graph.node for out in n.output}
|
| 52 |
+
|
| 53 |
+
# The int64 index chain of each deformable-conv sample, done in float instead (the values are whole numbers).
|
| 54 |
+
cast_nodes = [n for n in graph.node if n.op_type == "Cast" and attr(n, "to") == TensorProto.INT64]
|
| 55 |
+
region = set()
|
| 56 |
+
frontier = [n.output[0] for n in cast_nodes]
|
| 57 |
+
while frontier:
|
| 58 |
+
name = frontier.pop()
|
| 59 |
+
if name in region:
|
| 60 |
+
continue
|
| 61 |
+
region.add(name)
|
| 62 |
+
for c in consumers[name]:
|
| 63 |
+
if c.op_type in ("Slice", "Add", "Clip", "Reshape"):
|
| 64 |
+
frontier.append(c.output[0])
|
| 65 |
+
elif c.op_type == "Concat":
|
| 66 |
+
region.add(c.output[0])
|
| 67 |
+
elif c.op_type != "GatherND":
|
| 68 |
+
raise SystemExit(f"unexpected consumer {c.op_type} of int64 tensor {name}")
|
| 69 |
+
cast_source = {n.output[0]: n.input[0] for n in cast_nodes}
|
| 70 |
+
|
| 71 |
+
drop = set(id(n) for n in cast_nodes)
|
| 72 |
+
renames = {}
|
| 73 |
+
for n in graph.node:
|
| 74 |
+
if n.op_type in ("Add", "Clip") and n.output[0] in region:
|
| 75 |
+
for i, x in enumerate(n.input):
|
| 76 |
+
if x in inits and inits[x].data_type == TensorProto.INT64:
|
| 77 |
+
n.input[i] = const("float_const", numpy_helper.to_array(inits[x]).astype(np.float32))
|
| 78 |
+
for n in graph.node:
|
| 79 |
+
for i, x in enumerate(n.input):
|
| 80 |
+
if x in cast_source:
|
| 81 |
+
n.input[i] = cast_source[x]
|
| 82 |
+
|
| 83 |
+
out_nodes = []
|
| 84 |
+
stats = Counter()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def emit(op, inputs, outputs, **attrs):
|
| 88 |
+
out_nodes.append(helper.make_node(op, inputs, outputs, name=fresh(op.lower()), **attrs))
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def concat_tree(inputs, output, axis):
|
| 92 |
+
while len(inputs) > FAN:
|
| 93 |
+
grouped = []
|
| 94 |
+
for start in range(0, len(inputs), FAN):
|
| 95 |
+
group = inputs[start:start + FAN]
|
| 96 |
+
if len(group) == 1:
|
| 97 |
+
grouped.append(group[0])
|
| 98 |
+
continue
|
| 99 |
+
name = fresh("concat")
|
| 100 |
+
emit("Concat", group, [name], axis=axis)
|
| 101 |
+
grouped.append(name)
|
| 102 |
+
inputs = grouped
|
| 103 |
+
emit("Concat", inputs, [output], axis=axis)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def split_tree(source_name, sizes, outputs, axis):
|
| 107 |
+
if len(outputs) <= FAN:
|
| 108 |
+
emit("Split", [source_name, const("split_sizes", np.array(sizes, dtype=np.int64))], outputs, axis=axis)
|
| 109 |
+
return
|
| 110 |
+
size = math.ceil(len(outputs) / FAN)
|
| 111 |
+
parts, part_sizes = [], []
|
| 112 |
+
for start in range(0, len(outputs), size):
|
| 113 |
+
group_outputs, group_sizes = outputs[start:start + size], sizes[start:start + size]
|
| 114 |
+
name = group_outputs[0] if len(group_outputs) == 1 else fresh("split")
|
| 115 |
+
parts.append((name, group_sizes, group_outputs))
|
| 116 |
+
part_sizes.append(sum(group_sizes))
|
| 117 |
+
split_tree(source_name, part_sizes, [p[0] for p in parts], axis)
|
| 118 |
+
for name, group_sizes, group_outputs in parts:
|
| 119 |
+
if len(group_outputs) > 1:
|
| 120 |
+
split_tree(name, group_sizes, group_outputs, axis)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
concat_inputs = {n.output[0]: list(n.input) for n in graph.node if n.op_type == "Concat" and n.output[0] in region}
|
| 124 |
+
|
| 125 |
+
for n in graph.node:
|
| 126 |
+
if id(n) in drop:
|
| 127 |
+
continue
|
| 128 |
+
if n.op_type == "Concat" and n.output[0] in region:
|
| 129 |
+
stats["index concat removed"] += 1
|
| 130 |
+
continue
|
| 131 |
+
if n.op_type == "Concat" and len(n.input) > FAN:
|
| 132 |
+
concat_tree(list(n.input), n.output[0], attr(n, "axis"))
|
| 133 |
+
stats["concat"] += 1
|
| 134 |
+
continue
|
| 135 |
+
if n.op_type == "Split" and len(n.output) > FAN:
|
| 136 |
+
sizes = numpy_helper.to_array(inits[n.input[1]]).tolist()
|
| 137 |
+
axis = attr(n, "axis", 0)
|
| 138 |
+
split_tree(n.input[0], sizes, list(n.output), axis)
|
| 139 |
+
stats["split"] += 1
|
| 140 |
+
continue
|
| 141 |
+
if n.op_type == "Sum":
|
| 142 |
+
total = n.input[0]
|
| 143 |
+
for i, x in enumerate(n.input[1:], start=1):
|
| 144 |
+
name = n.output[0] if i == len(n.input) - 1 else fresh("sum")
|
| 145 |
+
emit("Add", [total, x], [name])
|
| 146 |
+
total = name
|
| 147 |
+
stats["sum"] += 1
|
| 148 |
+
continue
|
| 149 |
+
if n.op_type == "GatherND":
|
| 150 |
+
assert attr(n, "batch_dims") == 2, "only batch_dims 2 is handled"
|
| 151 |
+
data, index = n.input
|
| 152 |
+
data_shape = shapes[data][0]
|
| 153 |
+
assert data_shape[:2] == [1, 1], data_shape
|
| 154 |
+
_, _, height, width, channels = data_shape
|
| 155 |
+
y, x = concat_inputs[index]
|
| 156 |
+
count = shapes[n.output[0]][0][2]
|
| 157 |
+
y32, x32, row, flat, flat1, table, picked = (fresh(k) for k in ("y32", "x32", "row", "flat", "flat1", "table", "picked"))
|
| 158 |
+
emit("Cast", [y], [y32], to=TensorProto.INT32)
|
| 159 |
+
emit("Cast", [x], [x32], to=TensorProto.INT32)
|
| 160 |
+
emit("Mul", [y32, const("width", np.array(width, dtype=np.int32))], [row])
|
| 161 |
+
emit("Add", [row, x32], [flat])
|
| 162 |
+
emit("Reshape", [flat, const("shape", np.array([count], dtype=np.int64))], [flat1])
|
| 163 |
+
emit("Reshape", [data, const("shape", np.array([height * width, channels], dtype=np.int64))], [table])
|
| 164 |
+
emit("Gather", [table, flat1], [picked], axis=0)
|
| 165 |
+
emit("Reshape", [picked, const("shape", np.array([1, 1, count, channels], dtype=np.int64))], [n.output[0]])
|
| 166 |
+
stats["gathernd"] += 1
|
| 167 |
+
continue
|
| 168 |
+
out_nodes.append(n)
|
| 169 |
+
|
| 170 |
+
del graph.node[:]
|
| 171 |
+
graph.node.extend(out_nodes)
|
| 172 |
+
graph.initializer.extend(new_inits)
|
| 173 |
+
del graph.value_info[:]
|
| 174 |
+
used = {x for n in graph.node for x in n.input}
|
| 175 |
+
kept = [i for i in graph.initializer if i.name in used]
|
| 176 |
+
del graph.initializer[:]
|
| 177 |
+
graph.initializer.extend(kept)
|
| 178 |
+
del model.opset_import[:]
|
| 179 |
+
model.opset_import.extend([helper.make_opsetid("", 17)])
|
| 180 |
+
|
| 181 |
+
model = shape_inference.infer_shapes(model, strict_mode=True)
|
| 182 |
+
onnx.checker.check_model(model)
|
| 183 |
+
onnx.save(model, target)
|
| 184 |
+
print("rewrote", dict(stats))
|
| 185 |
+
|
| 186 |
+
# Audit: buffers per node, and anything WebGPU would run on the CPU.
|
| 187 |
+
graph = model.graph
|
| 188 |
+
types = {v.name: v.type.tensor_type.elem_type for v in list(graph.value_info) + list(graph.output) + list(graph.input)}
|
| 189 |
+
cpu_inputs = {("Reshape", 1), ("Split", 1), ("Slice", 1), ("Slice", 2), ("Slice", 3), ("Slice", 4), ("Clip", 1), ("Clip", 2), ("Pad", 1), ("Pad", 2), ("Resize", 1), ("Resize", 2), ("Resize", 3)}
|
| 190 |
+
worst = Counter()
|
| 191 |
+
for n in graph.node:
|
| 192 |
+
buffers = sum(1 for i, x in enumerate(n.input) if x and (n.op_type, i) not in cpu_inputs) + len(n.output)
|
| 193 |
+
worst[n.op_type] = max(worst[n.op_type], buffers)
|
| 194 |
+
print("most buffers per op:", dict(sorted(worst.items(), key=lambda kv: -kv[1])))
|
| 195 |
+
int64 = Counter(n.op_type for n in graph.node if any(types.get(o) == TensorProto.INT64 for o in n.output))
|
| 196 |
+
print("ops still making int64:", dict(int64) or "none")
|
| 197 |
+
print("ops:", dict(Counter(n.op_type for n in graph.node).most_common()))
|
scripts/to_fp16.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from collections import Counter
|
| 3 |
+
|
| 4 |
+
import onnx
|
| 5 |
+
from onnxconverter_common import float16
|
| 6 |
+
|
| 7 |
+
source, target = sys.argv[1], sys.argv[2]
|
| 8 |
+
model = float16.convert_float_to_float16(onnx.load(source), keep_io_types=True, disable_shape_infer=False)
|
| 9 |
+
onnx.checker.check_model(model)
|
| 10 |
+
onnx.save(model, target)
|
| 11 |
+
graph = model.graph
|
| 12 |
+
float_inits = Counter(onnx.TensorProto.DataType.Name(i.data_type) for i in graph.initializer)
|
| 13 |
+
print("initializer types", dict(float_inits))
|
| 14 |
+
print("casts", sum(1 for n in graph.node if n.op_type == "Cast"), "of", len(graph.node), "nodes")
|
scripts/verify.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Runs each model on the sample images on the CPU and compares its output with the first model's."""
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import onnxruntime as ort
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
samples_dir, out_dir, *models = sys.argv[1:]
|
| 11 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 12 |
+
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
| 13 |
+
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def pixels(path):
|
| 17 |
+
image = Image.open(path).convert("RGB")
|
| 18 |
+
resized = np.asarray(image.resize((1024, 1024), Image.BILINEAR), dtype=np.float32) / 255
|
| 19 |
+
return image, ((resized - MEAN) / STD).transpose(2, 0, 1)[None]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def sigmoid(x):
|
| 23 |
+
return 1 / (1 + np.exp(-x))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
samples = [(name, *pixels(os.path.join(samples_dir, name))) for name in sorted(os.listdir(samples_dir))]
|
| 27 |
+
reference = {}
|
| 28 |
+
for index, path in enumerate(models):
|
| 29 |
+
label = os.path.splitext(os.path.basename(path))[0]
|
| 30 |
+
options = ort.SessionOptions()
|
| 31 |
+
options.log_severity_level = 3
|
| 32 |
+
started = time.time()
|
| 33 |
+
session = ort.InferenceSession(path, options, providers=["CPUExecutionProvider"])
|
| 34 |
+
print(f"{label}: loaded in {time.time() - started:.1f} s, {os.path.getsize(path) / 1e6:.0f} MB")
|
| 35 |
+
input_name = session.get_inputs()[0].name
|
| 36 |
+
for name, image, x in samples:
|
| 37 |
+
started = time.time()
|
| 38 |
+
logits = session.run(None, {input_name: x})[0].astype(np.float32)[0, 0]
|
| 39 |
+
took = time.time() - started
|
| 40 |
+
alpha = sigmoid(logits)
|
| 41 |
+
Image.fromarray((alpha * 255).round().astype(np.uint8)).resize(image.size, Image.BILINEAR).save(os.path.join(out_dir, f"{os.path.splitext(name)[0]}-{label}.png"))
|
| 42 |
+
if index == 0:
|
| 43 |
+
reference[name] = (logits, alpha)
|
| 44 |
+
print(f" {name}: {took:.1f} s, logits {logits.min():.2f}..{logits.max():.2f}, {100 * (alpha > 0.5).mean():.1f}% foreground")
|
| 45 |
+
continue
|
| 46 |
+
ref_logits, ref_alpha = reference[name]
|
| 47 |
+
diff = np.abs(alpha - ref_alpha)
|
| 48 |
+
print(
|
| 49 |
+
f" {name}: {took:.1f} s, max logit diff {np.abs(logits - ref_logits).max():.2e}, "
|
| 50 |
+
f"alpha diff max {diff.max():.2e} mean {diff.mean():.2e}, "
|
| 51 |
+
f"pixels off by more than 1/255: {100 * (diff > 1 / 255).mean():.3f}%"
|
| 52 |
+
)
|