psiddh's picture
Add ExecuTorch WebGPU program for modnet
19fd35d verified
Raw History Blame
5.58 kB
#!/usr/bin/env python3
"""MODNet portrait matting -> ExecuTorch WebGPU.
The official code is on GitHub (ZHKKKe/MODNet) and the official checkpoints on Google
Drive; neither is reachable from the export host and the author has no Hugging Face
account. This pins a community Hugging Face mirror that carries a copy of the upstream
source tree and the photographic checkpoint (sha256 checked; four independent mirrors
publish the same checkpoint bytes). The webcam checkpoint comes from a second mirror.
Input: [1, 3, 256, 256] float32 RGB normalized to [-1, 1] ((x / 255 - 0.5) / 0.5).
Output: [1, 1, 256, 256] alpha matte in [0, 1] (inference=True, pred_matte only).
"""
import argparse
import importlib
import sys
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _common # noqa: E402
SOURCE = {
"repo": "boopathiraj/MODNet",
"revision": "a967175f8fd63de67a1c161d8daac966d2b57e3a",
"files": {
"modnet/__init__.py": None,
"modnet/modnet.py": None,
"modnet/src/__init__.py": None,
"modnet/src/models/__init__.py": None,
"modnet/src/models/backbones/__init__.py": None,
"modnet/src/models/backbones/wrapper.py": None,
"modnet/src/models/backbones/mobilenetv2.py": None,
"modnet/LICENSE": None,
},
}
CHECKPOINTS = {
"photographic": {
"repo": "boopathiraj/MODNet",
"revision": "a967175f8fd63de67a1c161d8daac966d2b57e3a",
"file": "modnet/pretrained/modnet_photographic_portrait_matting.ckpt",
"sha256": "7c22235f0925deba15d4d63e53afcb654c47055bbcd98f56e393ab2584007ed8",
},
"webcam": {
"repo": "DavG25/modnet-pretrained-models",
"revision": "903cc06311b3b12071edfa1b42b534e4a31c718a",
"file": "models/modnet_webcam_portrait_matting.ckpt",
"sha256": "913b82b66558db39b6286c150f809017d7528c872b156eb14333c9c6cb52108b",
},
}
class InstanceNorm2dDecomposed(torch.nn.Module):
"""InstanceNorm2d(affine=False) as mean/sub/mul/sqrt/div.
The Vulkan/WebGPU partitioner does not take aten._native_batch_norm_legit.no_stats,
so the stock module would leave 16 CPU kernels splitting the graph into 13 delegates.
"""
def __init__(self, eps):
super().__init__()
self.eps = eps
def forward(self, x):
d = x - x.mean(dim=(2, 3), keepdim=True)
var = (d * d).mean(dim=(2, 3), keepdim=True)
return d / torch.sqrt(var + self.eps)
def build_model(variant):
root = None
files = {}
for f in SOURCE["files"]:
path, digest = _common.hf_file(SOURCE["repo"], f, SOURCE["revision"])
files[f] = digest
root = Path(str(path)[: -len(f)])
sys.path.insert(0, str(root))
MODNet = importlib.import_module("modnet.modnet").MODNet
sys.path.remove(str(root))
ck = CHECKPOINTS[variant]
path, _ = _common.hf_file(ck["repo"], ck["file"], ck["revision"], ck["sha256"])
state = torch.load(path, map_location="cpu", weights_only=True)
# Checkpoints were saved from nn.DataParallel.
state = {k.removeprefix("module."): v for k, v in state.items()}
model = MODNet(backbone_pretrained=False)
model.load_state_dict(state, strict=True)
for parent in list(model.modules()):
for name, child in parent.named_children():
if isinstance(child, torch.nn.InstanceNorm2d):
assert not child.affine and not child.track_running_stats
setattr(parent, name, InstanceNorm2dDecomposed(child.eps))
class Matte(torch.nn.Module):
def __init__(self, m):
super().__init__()
self.m = m
def forward(self, img):
return self.m(img, True)[2]
return Matte(model).eval(), files
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--out", required=True, help="output directory")
ap.add_argument("--size", type=int, nargs=2, default=[256, 256], metavar=("H", "W"))
ap.add_argument("--variant", choices=sorted(CHECKPOINTS), default="photographic")
ap.add_argument(
"--golden",
help="optional goldens.manifest.json to compare against (key: bgremove)",
)
args = ap.parse_args()
model, source_files = build_model(args.variant)
golden = _common.load_golden(args.golden, "bgremove") if args.golden else None
_common.export_vision(
name="modnet",
model=model,
example_input=torch.rand(1, 3, *args.size) * 2 - 1,
out_dir=args.out,
upstream={
"model": f"ZHKKKe/MODNet ({args.variant} portrait matting checkpoint)",
"weights": {
**CHECKPOINTS[args.variant],
"official": False,
"note": "community mirror; official checkpoints are on Google Drive, linked from GitHub, neither reachable from the export host",
},
"architecture_source": {
"repo": SOURCE["repo"],
"revision": SOURCE["revision"],
"files_sha256": source_files,
"official": False,
"note": "copy of the upstream src/models tree with modnet.py moved to the package root",
},
"license": "Apache-2.0",
},
extra_recipe={
"variant": args.variant,
"preprocessing_outside_graph": "RGB / 255, then (x - 0.5) / 0.5",
"postprocessing_outside_graph": "alpha in [0, 1]",
},
golden=golden,
)
if __name__ == "__main__":
main()