Instructions to use edelfrade/BiRefNet_lite-webgpu with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers.js
How to use edelfrade/BiRefNet_lite-webgpu with Transformers.js:
// npm i @huggingface/transformers import { pipeline } from '@huggingface/transformers'; // Allocate pipeline const pipe = await pipeline('image-segmentation', 'edelfrade/BiRefNet_lite-webgpu');
BiRefNet-lite ONNX rebuilt for WebGPU (≤7 storage buffers per node)
Browse files- README.md +45 -0
- config.json +6 -0
- onnx/model.onnx +3 -0
- onnx/model_fp16.onnx +3 -0
- preprocessor_config.json +23 -0
- 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()
|