Depth-Anything-V2-Small-ExecuTorch-WebGPU / export /depth-anything-v2-small.py
psiddh's picture
Add ExecuTorch WebGPU program for depth-anything-v2-small
f6696e9 verified
Raw History Blame Contribute Delete
4.39 kB
#!/usr/bin/env python3
"""Depth Anything V2 Small (Hugging Face transformers port) -> ExecuTorch WebGPU.
Upstream: depth-anything/Depth-Anything-V2-Small-hf (Apache-2.0; the Base and Large
variants are CC BY-NC 4.0 and must not be used here).
Input: [1, 3, 224, 224] float32 RGB, ImageNet-normalized outside the graph
((x / 255 - mean) / std, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)).
Output: [1, 224, 224] relative inverse depth (`predicted_depth`).
"""
import argparse
import sys
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _common # noqa: E402
UPSTREAM = {
"repo": "depth-anything/Depth-Anything-V2-Small-hf",
"revision": "5426e4f0f36572d16453bbda7a8389317b1bef99",
}
class PredictedDepth(torch.nn.Module):
def __init__(self, m):
super().__init__()
self.m = m
def forward(self, pixel_values):
return self.m(pixel_values=pixel_values).predicted_depth
def freeze_position_embeddings(model, height, width):
"""Bake the bicubic-resized DINOv2 position embeddings for one input size into a buffer.
Traced live, the resize leaves lifted tensor constants and aten.arange nodes whose fake
tensors belong to a different FakeTensorMode, and the Vulkan preprocess passes fail on
them. The values are what the eager module computes for this size.
"""
import types
emb = model.m.backbone.embeddings
with torch.no_grad():
dummy = torch.zeros(1, (height // 14) * (width // 14) + 1, emb.position_embeddings.shape[-1])
frozen = emb.interpolate_pos_encoding(dummy, height, width).detach().clone()
emb.register_buffer("frozen_position_embeddings", frozen, persistent=False)
emb.interpolate_pos_encoding = types.MethodType(lambda self, e, h, w: self.frozen_position_embeddings, emb)
return model
def build_model(attn):
from transformers import AutoModelForDepthEstimation
from transformers.models.dinov2 import modeling_dinov2
# transformers 5.x materializes an all-zero additive mask for eager attention even
# without padding (arange/ge/where plus a CPU scalar_tensor in the graph). Images never
# pad, so drop it; outputs are unchanged.
create_mask = modeling_dinov2.create_bidirectional_mask
modeling_dinov2.create_bidirectional_mask = lambda **kw: (
None if kw.get("attention_mask") is None else create_mask(**kw)
)
model = AutoModelForDepthEstimation.from_pretrained(
UPSTREAM["repo"],
revision=UPSTREAM["revision"],
attn_implementation=attn,
torch_dtype=torch.float32,
)
return PredictedDepth(model.eval()).eval()
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=[224, 224],
metavar=("H", "W"),
help="multiples of 14",
)
ap.add_argument(
"--attn",
default="eager",
choices=["eager", "sdpa"],
help="attention implementation traced into the graph",
)
ap.add_argument(
"--golden",
help="optional goldens.manifest.json to compare against (key: depth)",
)
args = ap.parse_args()
golden = _common.load_golden(args.golden, "depth") if args.golden else None
_common.export_vision(
name="depth_v2s",
model=freeze_position_embeddings(build_model(args.attn), *args.size),
example_input=torch.randn(1, 3, *args.size),
out_dir=args.out,
upstream={
"model": UPSTREAM["repo"],
"weights": {**UPSTREAM, "official": True},
"license": "Apache-2.0",
},
extra_recipe={
"attn_implementation": args.attn,
"graph_rewrites": ["transformers 5.x all-zero bidirectional attention mask skipped (attention_mask=None; no padding in images)", "DINOv2 position embeddings resized (bicubic, as upstream) for the fixed input size in eager and stored as a constant"],
"preprocessing_outside_graph": "RGB / 255, ImageNet mean/std normalization, resize to input size",
"postprocessing_outside_graph": "min-max normalize predicted_depth for display",
},
golden=golden,
)
if __name__ == "__main__":
main()