edelfrade commited on
Commit
20f6e53
·
verified ·
1 Parent(s): 7346fd2

BiRefNet-lite ONNX rebuilt for WebGPU (≤7 storage buffers per node)

Browse files
Files changed (6) hide show
  1. README.md +45 -0
  2. config.json +6 -0
  3. onnx/model.onnx +3 -0
  4. onnx/model_fp16.onnx +3 -0
  5. preprocessor_config.json +23 -0
  6. rebuild.py +162 -0
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers.js
3
+ tags:
4
+ - background-removal
5
+ - image-segmentation
6
+ - webgpu
7
+ - onnx
8
+ pipeline_tag: image-segmentation
9
+ license: mit
10
+ base_model:
11
+ - ZhengPeng7/BiRefNet_lite
12
+ - onnx-community/BiRefNet_lite-ONNX
13
+ ---
14
+
15
+ # BiRefNet-lite ONNX, rebuilt for WebGPU
16
+
17
+ [BiRefNet-lite](https://github.com/ZhengPeng7/BiRefNet) (MIT) for Transformers.js, rebuilt so it runs on **any WebGPU device**.
18
+
19
+ The ONNX export in [onnx-community/BiRefNet_lite-ONNX](https://huggingface.co/onnx-community/BiRefNet_lite-ONNX) has Concat nodes with up to 1024 inputs and Split nodes with up to 32 outputs. ONNX Runtime's WebGPU backend binds each input and output as a storage buffer, and GPUs limit that per shader (`maxStorageBuffersPerShaderStage`: 8 minimum, often 10 or 16), so those nodes fail with:
20
+
21
+ ```
22
+ Too many storage buffers in shader. Current: 17, Max is 16
23
+ ```
24
+
25
+ This repo replaces those 4 Concat and 59 Split nodes with trees of smaller ones (at most 6 inputs per Concat, 5 outputs per Split, so every node uses ≤ 7 storage buffers). Nothing else changes: the outputs are **bit-for-bit identical** to the original on ONNX Runtime CPU, for both files.
26
+
27
+ | File | Precision | Size | Needs |
28
+ |---|---|---|---|
29
+ | `onnx/model_fp16.onnx` | fp16 | 115 MB | WebGPU with `shader-f16` |
30
+ | `onnx/model.onnx` | fp32 | 224 MB | WebGPU |
31
+
32
+ Reproduce with `rebuild.py` (included): `python rebuild.py model.onnx out.onnx` on the files from `onnx-community/BiRefNet_lite-ONNX` (source SHA-256: fp32 `5600024376f572a557870a5eb0afb1e5961636bef4e1e22132025467d0f03333`, fp16 `d39b897ceb16ae654c1731f3dba0cf9b368d9cae74b5a57459b455cc8bfec402`).
33
+
34
+ ## Usage (Transformers.js)
35
+
36
+ ```js
37
+ import { pipeline } from "@huggingface/transformers";
38
+
39
+ const segmenter = await pipeline("image-segmentation", "edelfrade/BiRefNet_lite-webgpu", { device: "webgpu", dtype: "fp16" });
40
+ const [{ mask }] = await segmenter(image);
41
+ ```
42
+
43
+ ## License
44
+
45
+ MIT, same as the original model. Credit to Peng Zheng et al., *Bilateral Reference for High-Resolution Dichotomous Image Segmentation* (CAAI AIR 2024), and to onnx-community for the ONNX export.
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:a9e7aebec91bad0cf3949015e8ddc379602db6a2b7a06918e81340f4e0ce3716
3
+ size 224182130
onnx/model_fp16.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e41401436e7e9a074f0f06556f5296702d48c8a0a227fb8a6f9ce4c7d08166f3
3
+ size 114715263
preprocessor_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "do_rescale": true,
4
+ "do_resize": true,
5
+ "feature_extractor_type": "ViTFeatureExtractor",
6
+ "image_mean": [
7
+ 0.485,
8
+ 0.456,
9
+ 0.406
10
+ ],
11
+ "image_processor_type": "ViTFeatureExtractor",
12
+ "image_std": [
13
+ 0.229,
14
+ 0.224,
15
+ 0.225
16
+ ],
17
+ "resample": 2,
18
+ "rescale_factor": 0.00392156862745098,
19
+ "size": {
20
+ "height": 1024,
21
+ "width": 1024
22
+ }
23
+ }
rebuild.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Rebuild BiRefNet-lite ONNX so it runs on any WebGPU device.
3
+
4
+ The official export has Concat nodes with up to 1024 inputs and Split nodes
5
+ with up to 32 outputs. ONNX Runtime's WebGPU backend binds every input and
6
+ output of a node as a storage buffer, and GPUs cap that per shader
7
+ (maxStorageBuffersPerShaderStage: 8 minimum, 10 or 16 on many devices), so
8
+ those nodes fail with "Too many storage buffers in shader".
9
+
10
+ This replaces them with trees of small nodes that compute exactly the same
11
+ values (concatenation and splitting are associative along one axis):
12
+ - Concat: at most CONCAT_FAN_IN inputs per node (+1 output)
13
+ - Split: at most SPLIT_FAN_OUT outputs per node (+ data + sizes inputs)
14
+ Every node then uses at most MAX_BUFFERS storage buffers.
15
+
16
+ Usage: python rebuild.py <in.onnx> <out.onnx>
17
+ """
18
+
19
+ import sys
20
+
21
+ import numpy as np
22
+ import onnx
23
+ from onnx import helper, numpy_helper
24
+
25
+ CONCAT_FAN_IN = 6
26
+ SPLIT_FAN_OUT = 5
27
+ MAX_BUFFERS = 7
28
+
29
+
30
+ def buffers(node: onnx.NodeProto) -> int:
31
+ return len([i for i in node.input if i]) + len([o for o in node.output if o])
32
+
33
+
34
+ class Rewriter:
35
+ def __init__(self, model: onnx.ModelProto):
36
+ self.model = model
37
+ self.graph = model.graph
38
+ self.counter = 0
39
+ self.initializers = {t.name: t for t in self.graph.initializer}
40
+ self.constants = {
41
+ n.output[0]: n for n in self.graph.node if n.op_type == "Constant" and len(n.output) == 1
42
+ }
43
+ self.names = {o for n in self.graph.node for o in n.output} | set(self.initializers)
44
+ self.names |= {i.name for i in self.graph.input}
45
+
46
+ def fresh(self, base: str) -> str:
47
+ while True:
48
+ self.counter += 1
49
+ name = f"{base}/wgpu_{self.counter}"
50
+ if name not in self.names:
51
+ self.names.add(name)
52
+ return name
53
+
54
+ def const_value(self, name: str) -> np.ndarray:
55
+ if name in self.initializers:
56
+ return numpy_helper.to_array(self.initializers[name])
57
+ if name in self.constants:
58
+ attr = next(a for a in self.constants[name].attribute if a.name == "value")
59
+ return numpy_helper.to_array(attr.t)
60
+ raise ValueError(f"split sizes '{name}' are not constant")
61
+
62
+ def int64_initializer(self, base: str, values: list[int]) -> str:
63
+ name = self.fresh(base + "/sizes")
64
+ tensor = numpy_helper.from_array(np.array(values, dtype=np.int64), name)
65
+ self.graph.initializer.append(tensor)
66
+ self.initializers[name] = tensor
67
+ return name
68
+
69
+ def concat_tree(self, node: onnx.NodeProto) -> list[onnx.NodeProto]:
70
+ axis = next(a.i for a in node.attribute if a.name == "axis")
71
+ inputs = list(node.input)
72
+ made: list[onnx.NodeProto] = []
73
+ while len(inputs) > CONCAT_FAN_IN:
74
+ grouped = []
75
+ for start in range(0, len(inputs), CONCAT_FAN_IN):
76
+ chunk = inputs[start : start + CONCAT_FAN_IN]
77
+ if len(chunk) == 1:
78
+ grouped.append(chunk[0])
79
+ continue
80
+ out = self.fresh(node.name)
81
+ made.append(helper.make_node("Concat", chunk, [out], name=self.fresh(node.name), axis=axis))
82
+ grouped.append(out)
83
+ inputs = grouped
84
+ made.append(helper.make_node("Concat", inputs, list(node.output), name=node.name, axis=axis))
85
+ return made
86
+
87
+ def split_tree(self, node: onnx.NodeProto) -> list[onnx.NodeProto]:
88
+ axis = next((a.i for a in node.attribute if a.name == "axis"), 0)
89
+ if len(node.input) < 2 or not node.input[1]:
90
+ raise ValueError(f"{node.name}: Split without explicit sizes isn't handled")
91
+ sizes = [int(v) for v in self.const_value(node.input[1]).tolist()]
92
+ outputs = list(node.output)
93
+ assert len(sizes) == len(outputs)
94
+ made: list[onnx.NodeProto] = []
95
+
96
+ def split(data: str, parts: list[tuple[int, str]], base: str) -> None:
97
+ # parts: (size, final output name)
98
+ if len(parts) <= SPLIT_FAN_OUT:
99
+ made.append(
100
+ helper.make_node(
101
+ "Split",
102
+ [data, self.int64_initializer(base, [s for s, _ in parts])],
103
+ [o for _, o in parts],
104
+ name=self.fresh(base),
105
+ axis=axis,
106
+ )
107
+ )
108
+ return
109
+ # Groups of roughly equal count, each split again below.
110
+ per_group = -(-len(parts) // SPLIT_FAN_OUT)
111
+ groups = [parts[i : i + per_group] for i in range(0, len(parts), per_group)]
112
+ group_outputs = []
113
+ for group in groups:
114
+ group_outputs.append(group[0][1] if len(group) == 1 else self.fresh(base))
115
+ made.append(
116
+ helper.make_node(
117
+ "Split",
118
+ [data, self.int64_initializer(base, [sum(s for s, _ in g) for g in groups])],
119
+ group_outputs,
120
+ name=self.fresh(base),
121
+ axis=axis,
122
+ )
123
+ )
124
+ for group, out in zip(groups, group_outputs):
125
+ if len(group) > 1:
126
+ split(out, group, base)
127
+
128
+ split(node.input[0], list(zip(sizes, outputs)), node.name)
129
+ return made
130
+
131
+ def run(self) -> dict[str, int]:
132
+ stats = {"Concat": 0, "Split": 0}
133
+ new_nodes: list[onnx.NodeProto] = []
134
+ for node in self.graph.node:
135
+ if buffers(node) > MAX_BUFFERS and node.op_type == "Concat":
136
+ new_nodes.extend(self.concat_tree(node))
137
+ stats["Concat"] += 1
138
+ elif buffers(node) > MAX_BUFFERS and node.op_type == "Split":
139
+ new_nodes.extend(self.split_tree(node))
140
+ stats["Split"] += 1
141
+ else:
142
+ new_nodes.append(node)
143
+ del self.graph.node[:]
144
+ self.graph.node.extend(new_nodes)
145
+ return stats
146
+
147
+
148
+ def main() -> None:
149
+ source, target = sys.argv[1], sys.argv[2]
150
+ model = onnx.load(source)
151
+ stats = Rewriter(model).run()
152
+ too_wide = [(n.op_type, n.name, buffers(n)) for n in model.graph.node if buffers(n) > MAX_BUFFERS]
153
+ if too_wide:
154
+ raise SystemExit(f"still too wide: {too_wide[:10]}")
155
+ onnx.checker.check_model(model)
156
+ onnx.save(model, target)
157
+ widest = max(buffers(n) for n in model.graph.node)
158
+ print(f"{source} -> {target}: rewrote {stats}, nodes {len(model.graph.node)}, widest node {widest} buffers")
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()