Re-export with public ExecuTorch 1.5.1 (recipe in export/)
Browse files- NOTICE +1 -1
- README.md +28 -20
- export/_common.py +330 -0
- export/_sam2.py +288 -0
- export/recipe.json +589 -0
- export/requirements.txt +15 -0
- export/sam2-hiera-tiny.py +939 -0
- manifest.json +4 -4
- sam2_decoder.pte +2 -2
- sam2_encoder.pte +2 -2
NOTICE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
This repository contains a modified version of facebook/sam2-hiera-tiny.
|
| 2 |
|
| 3 |
Modifications by Meta Platforms, Inc.: conversion to the ExecuTorch program format (.pte) for the
|
| 4 |
ExecuTorch WebGPU backend.
|
|
|
|
| 1 |
+
This repository contains a modified version of facebook/sam2.1-hiera-tiny.
|
| 2 |
|
| 3 |
Modifications by Meta Platforms, Inc.: conversion to the ExecuTorch program format (.pte) for the
|
| 4 |
ExecuTorch WebGPU backend.
|
README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
base_model:
|
| 4 |
-
- facebook/sam2-hiera-tiny
|
| 5 |
library_name: executorch
|
| 6 |
pipeline_tag: mask-generation
|
| 7 |
inference: false
|
|
@@ -11,27 +11,27 @@ tags:
|
|
| 11 |
- on-device
|
| 12 |
- browser
|
| 13 |
---
|
| 14 |
-
# SAM 2 Hiera Tiny for ExecuTorch WebGPU
|
| 15 |
|
| 16 |
This repository contains an [ExecuTorch](https://github.com/pytorch/executorch) conversion of
|
| 17 |
-
[facebook/sam2-hiera-tiny](https://huggingface.co/facebook/sam2-hiera-tiny) for the ExecuTorch **WebGPU** backend, so the model runs entirely in a web browser on
|
| 18 |
the visitor's GPU. It powers the corresponding demo in the ExecuTorch WebGPU Playground (launching soon on [executorch.ai](https://executorch.ai)).
|
| 19 |
|
| 20 |
-
- **Base model:** [facebook/sam2-hiera-tiny](https://huggingface.co/facebook/sam2-hiera-tiny)
|
| 21 |
- **Format:** ExecuTorch `.pte` programs delegated to the WebGPU backend
|
| 22 |
-
- **Quantization:** fp32 (
|
| 23 |
-
- **Total size:**
|
| 24 |
|
| 25 |
## About the original model
|
| 26 |
|
| 27 |
-
The tiny Hiera variant of SAM 2, a foundation model for promptable visual segmentation in images and videos.
|
| 28 |
|
| 29 |
- **Developed by:** Meta FAIR
|
| 30 |
- **Paper:** [SAM 2: Segment Anything in Images and Videos](https://arxiv.org/abs/2408.00714)
|
| 31 |
-
-
|
| 32 |
- Segments any object from point or box prompts
|
| 33 |
|
| 34 |
-
For training data, evaluation results and full details, see the original model card: [facebook/sam2-hiera-tiny](https://huggingface.co/facebook/sam2-hiera-tiny).
|
| 35 |
|
| 36 |
## Intended use and limitations
|
| 37 |
|
|
@@ -41,14 +41,18 @@ This conversion covers single-image segmentation only (no video memory). Masks c
|
|
| 41 |
|
| 42 |
## What changed in this conversion
|
| 43 |
|
| 44 |
-
- Image encoder and mask decoder exported as separate programs
|
| 45 |
-
- Fixed 512x512 input (upstream: 1024x1024)
|
| 46 |
-
-
|
|
|
|
|
|
|
| 47 |
|
| 48 |
## Inputs and outputs
|
| 49 |
|
| 50 |
-
- `sam2_encoder.pte`:
|
| 51 |
-
- `sam2_decoder.pte`:
|
|
|
|
|
|
|
| 52 |
|
| 53 |
## Files
|
| 54 |
|
|
@@ -62,16 +66,17 @@ This conversion covers single-image segmentation only (no video memory). Masks c
|
|
| 62 |
| `prompt_not_a_point.bin` | 1.0 KB | `19771c067b922d9d…` |
|
| 63 |
| `prompt_point_embed_bg.bin` | 1.0 KB | `91c432d10fcef8e3…` |
|
| 64 |
| `prompt_point_embed_fg.bin` | 1.0 KB | `2e037dab5bee002d…` |
|
| 65 |
-
| `sam2_decoder.pte` | 16.2 MB | `
|
| 66 |
-
| `sam2_encoder.pte` |
|
| 67 |
|
| 68 |
Full checksums are in `manifest.json`. The playground verifies them after download.
|
| 69 |
|
| 70 |
## Running it
|
| 71 |
|
| 72 |
These programs were exported with the ExecuTorch WebGPU backend
|
| 73 |
-
([backends/webgpu](https://github.com/pytorch/executorch/tree/main/backends/webgpu)) and
|
| 74 |
-
WebAssembly runtime build used by the ExecuTorch WebGPU Playground (launching soon on [executorch.ai](https://executorch.ai))
|
|
|
|
| 75 |
the playground is the supported way to run them. It downloads these files once, caches them in the browser,
|
| 76 |
and runs everything locally.
|
| 77 |
|
|
@@ -79,8 +84,11 @@ Requirements: a browser with WebGPU enabled.
|
|
| 79 |
|
| 80 |
## Provenance
|
| 81 |
|
| 82 |
-
-
|
| 83 |
-
-
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
## Citation
|
| 86 |
|
|
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
base_model:
|
| 4 |
+
- facebook/sam2.1-hiera-tiny
|
| 5 |
library_name: executorch
|
| 6 |
pipeline_tag: mask-generation
|
| 7 |
inference: false
|
|
|
|
| 11 |
- on-device
|
| 12 |
- browser
|
| 13 |
---
|
| 14 |
+
# SAM 2.1 Hiera Tiny for ExecuTorch WebGPU
|
| 15 |
|
| 16 |
This repository contains an [ExecuTorch](https://github.com/pytorch/executorch) conversion of
|
| 17 |
+
[facebook/sam2.1-hiera-tiny](https://huggingface.co/facebook/sam2.1-hiera-tiny) for the ExecuTorch **WebGPU** backend, so the model runs entirely in a web browser on
|
| 18 |
the visitor's GPU. It powers the corresponding demo in the ExecuTorch WebGPU Playground (launching soon on [executorch.ai](https://executorch.ai)).
|
| 19 |
|
| 20 |
+
- **Base model:** [facebook/sam2.1-hiera-tiny](https://huggingface.co/facebook/sam2.1-hiera-tiny)
|
| 21 |
- **Format:** ExecuTorch `.pte` programs delegated to the WebGPU backend
|
| 22 |
+
- **Quantization:** fp32 (no quantization)
|
| 23 |
+
- **Total size:** 128.1 MB
|
| 24 |
|
| 25 |
## About the original model
|
| 26 |
|
| 27 |
+
The tiny Hiera variant of SAM 2.1, the improved checkpoint release of SAM 2, a foundation model for promptable visual segmentation in images and videos.
|
| 28 |
|
| 29 |
- **Developed by:** Meta FAIR
|
| 30 |
- **Paper:** [SAM 2: Segment Anything in Images and Videos](https://arxiv.org/abs/2408.00714)
|
| 31 |
+
- 39.0M parameters
|
| 32 |
- Segments any object from point or box prompts
|
| 33 |
|
| 34 |
+
For training data, evaluation results and full details, see the original model card: [facebook/sam2.1-hiera-tiny](https://huggingface.co/facebook/sam2.1-hiera-tiny).
|
| 35 |
|
| 36 |
## Intended use and limitations
|
| 37 |
|
|
|
|
| 41 |
|
| 42 |
## What changed in this conversion
|
| 43 |
|
| 44 |
+
- Image encoder and mask decoder exported as two separate programs; the prompt encoder runs on the host from `prompt_*.bin` tables generated from the upstream weights
|
| 45 |
+
- Fixed 512x512 input (upstream: 1024x1024), so masks are 128x128 (upstream: 256x256)
|
| 46 |
+
- The encoder adds `no_memory_embedding` to the image embedding, as upstream does for single images
|
| 47 |
+
- Three prompt tokens: a click is [point, padding, padding] and a box is [top-left, bottom-right, padding]. The decoder ignores a padding token that follows another padding token, so every prompt type matches upstream
|
| 48 |
+
- No mask-prompt input. All weights fp32. Attention uses ExecuTorch's fused `et_vk.sdpa` op (within 5e-6 of PyTorch attention), and Hiera's windowed max-pooling runs as one batched pool (exact)
|
| 49 |
|
| 50 |
## Inputs and outputs
|
| 51 |
|
| 52 |
+
- `sam2_encoder.pte`: `forward(pixels float32 [1,3,512,512])`, an RGB image resized to 512x512 (aspect ratio not kept), scaled to [0, 1] and ImageNet-normalized (mean 0.485/0.456/0.406, std 0.229/0.224/0.225) -> `image_embeddings [1,256,32,32]`, `hr0 [1,256,128,128]`, `hr1 [1,256,64,64]` (high-resolution features)
|
| 53 |
+
- `sam2_decoder.pte`: `forward(image_embeddings, image_pe [1,256,32,32], sparse [1,1,3,256], dense [1,256,32,32], hr0, hr1)` -> `masks [1,3,128,128]` (logits of 3 candidate masks), `iou_scores [1,1,3]` (predicted IoU, after sigmoid), `object_score_logits [1,1,1]`
|
| 54 |
+
- `image_pe` is `prompt_img_pos.bin` and `dense` is `prompt_dense.bin` (no mask prompt). Each sparse token is `[sin(2πcG), cos(2πcG)] + e`, where `G` [2,128] is `prompt_gaussian.bin`, `c` is the point mapped to the 512 grid (+0.5 pixel) and normalized to [-1, 1], and `e` is `prompt_point_embed_fg.bin` / `prompt_point_embed_bg.bin` / `prompt_box_tl.bin` / `prompt_box_br.bin`; a padding token is `prompt_not_a_point.bin` alone; fill unused slots with padding tokens
|
| 55 |
+
- All tensors are float32, contiguous NCHW; `.bin` files are raw little-endian float32
|
| 56 |
|
| 57 |
## Files
|
| 58 |
|
|
|
|
| 66 |
| `prompt_not_a_point.bin` | 1.0 KB | `19771c067b922d9d…` |
|
| 67 |
| `prompt_point_embed_bg.bin` | 1.0 KB | `91c432d10fcef8e3…` |
|
| 68 |
| `prompt_point_embed_fg.bin` | 1.0 KB | `2e037dab5bee002d…` |
|
| 69 |
+
| `sam2_decoder.pte` | 16.2 MB | `57b91034acc71be2…` |
|
| 70 |
+
| `sam2_encoder.pte` | 109.9 MB | `30f9fb469b779558…` |
|
| 71 |
|
| 72 |
Full checksums are in `manifest.json`. The playground verifies them after download.
|
| 73 |
|
| 74 |
## Running it
|
| 75 |
|
| 76 |
These programs were exported with the ExecuTorch WebGPU backend
|
| 77 |
+
([backends/webgpu](https://github.com/pytorch/executorch/tree/main/backends/webgpu)) and checked against the upstream transformers model at 512x512 (click and box prompts give masks with IoU 1.000),
|
| 78 |
+
and run in a browser on WebGPU with the WebAssembly runtime build used by the ExecuTorch WebGPU Playground (launching soon on [executorch.ai](https://executorch.ai)), where the masks match the CPU run with IoU 1.000.
|
| 79 |
+
They have not yet been tested on a hardware GPU. That runtime build is not published yet; until it is,
|
| 80 |
the playground is the supported way to run them. It downloads these files once, caches them in the browser,
|
| 81 |
and runs everything locally.
|
| 82 |
|
|
|
|
| 84 |
|
| 85 |
## Provenance
|
| 86 |
|
| 87 |
+
- **Upstream:** `facebook/sam2.1-hiera-tiny` at revision `de431c4043854a71d8101e17995dfe596bf101a5` (official)
|
| 88 |
+
- **Checked against the official checkpoint:** `model.safetensors` (sha256 `48c14467e5cf9e51870511feb72c89688e82dd74523142c0538b663e193ac2a7`) has 469/469 tensors bit-identical to the authors' original checkpoint `sam2.1_hiera_tiny.pt` (sha256 `7402e0d864fa82708a20fbd15bc84245c2f26dff0eb43a4b5b93452deb34be69`) in the same repo at the same revision; `python export/_sam2.py verify-official` repeats the check
|
| 89 |
+
- **Exported with:** executorch 1.5.1+cpu, torch 2.14.1+cpu, torchao 0.18.0+cpu, transformers 5.19.0 (executorch git `3b6068392324`)
|
| 90 |
+
- **Reproduce:** `python recipes/sam2-hiera-tiny.py --out out/sam2.1-hiera-tiny` (recipe in `export/`)
|
| 91 |
+
- Re-running the recipe produces byte-identical files.
|
| 92 |
|
| 93 |
## Citation
|
| 94 |
|
export/_common.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared helpers for the ExecuTorch WebGPU export recipes in this directory.
|
| 2 |
+
|
| 3 |
+
Each recipe builds an eager model from a pinned upstream revision and calls
|
| 4 |
+
`export_vision` (or its own LLM path), which writes:
|
| 5 |
+
|
| 6 |
+
<out>/<name>_webgpu.pte program lowered with WebGPUPartitioner (delegate id "VulkanBackend")
|
| 7 |
+
<out>/<name>_portable.pte same exported graph without a delegate, for CPU validation only
|
| 8 |
+
<out>/recipe.json upstream id + revision, versions, shapes, dtype, command line
|
| 9 |
+
<out>/validation.json eager vs exported-program vs portable .pte (and goldens, if given)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import hashlib
|
| 13 |
+
import io
|
| 14 |
+
import json
|
| 15 |
+
import platform
|
| 16 |
+
import shlex
|
| 17 |
+
import sys
|
| 18 |
+
import urllib.request
|
| 19 |
+
import zipfile
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
PROXY_HINT = (
|
| 26 |
+
"set https_proxy if the machine needs a proxy for huggingface.co / pypi.org"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def versions():
|
| 31 |
+
import executorch
|
| 32 |
+
import torchao
|
| 33 |
+
from executorch.version import git_version
|
| 34 |
+
|
| 35 |
+
out = {
|
| 36 |
+
"python": platform.python_version(),
|
| 37 |
+
"torch": torch.__version__,
|
| 38 |
+
"torchao": torchao.__version__,
|
| 39 |
+
"executorch": getattr(executorch, "__version__", None)
|
| 40 |
+
or __import__("executorch.version").version.__version__,
|
| 41 |
+
"executorch_git": git_version,
|
| 42 |
+
}
|
| 43 |
+
try:
|
| 44 |
+
import transformers
|
| 45 |
+
|
| 46 |
+
out["transformers"] = transformers.__version__
|
| 47 |
+
except ImportError:
|
| 48 |
+
pass
|
| 49 |
+
try:
|
| 50 |
+
import huggingface_hub
|
| 51 |
+
|
| 52 |
+
out["huggingface_hub"] = huggingface_hub.__version__
|
| 53 |
+
except ImportError:
|
| 54 |
+
pass
|
| 55 |
+
return out
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def sha256_file(path):
|
| 59 |
+
h = hashlib.sha256()
|
| 60 |
+
with open(path, "rb") as fh:
|
| 61 |
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
| 62 |
+
h.update(chunk)
|
| 63 |
+
return h.hexdigest()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def hf_file(repo_id, filename, revision, expected_sha256=None, cache_dir=None):
|
| 67 |
+
from huggingface_hub import hf_hub_download
|
| 68 |
+
|
| 69 |
+
path = hf_hub_download(
|
| 70 |
+
repo_id=repo_id, filename=filename, revision=revision, cache_dir=cache_dir
|
| 71 |
+
)
|
| 72 |
+
digest = sha256_file(path)
|
| 73 |
+
if expected_sha256 and digest != expected_sha256:
|
| 74 |
+
sys.exit(
|
| 75 |
+
f"{repo_id}@{revision}:{filename} sha256 {digest} != {expected_sha256}"
|
| 76 |
+
)
|
| 77 |
+
return Path(path), digest
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def pypi_wheel_member(url, expected_sha256, member):
|
| 81 |
+
"""Read one file out of a PyPI wheel, verifying the wheel's sha256."""
|
| 82 |
+
with urllib.request.urlopen(url, timeout=120) as resp:
|
| 83 |
+
blob = resp.read()
|
| 84 |
+
digest = hashlib.sha256(blob).hexdigest()
|
| 85 |
+
if digest != expected_sha256:
|
| 86 |
+
sys.exit(f"{url}: sha256 {digest} != {expected_sha256} ({PROXY_HINT})")
|
| 87 |
+
with zipfile.ZipFile(io.BytesIO(blob)) as zf:
|
| 88 |
+
return zf.read(member).decode()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def scrub(text, out_dir):
|
| 92 |
+
"""Replace machine-specific paths so recipe.json can ship with the model."""
|
| 93 |
+
text = text.replace(str(Path(out_dir).resolve()), "<out>")
|
| 94 |
+
text = text.replace(str(Path(__file__).resolve().parent), "recipes")
|
| 95 |
+
return text.replace(str(Path.home()), "~")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def command_line(out_dir):
|
| 99 |
+
argv = [f"recipes/{Path(sys.argv[0]).name}", *sys.argv[1:]]
|
| 100 |
+
args = []
|
| 101 |
+
for a in argv:
|
| 102 |
+
if a.startswith("/") or a.startswith("."):
|
| 103 |
+
a = scrub(str(Path(a).resolve()), out_dir)
|
| 104 |
+
args.append(a)
|
| 105 |
+
return scrub(" ".join(["python", *(shlex.quote(a) for a in args)]), out_dir)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def lower_webgpu(ep):
|
| 109 |
+
from executorch.backends.webgpu.partitioner import WebGPUPartitioner
|
| 110 |
+
from executorch.exir import to_edge_transform_and_lower
|
| 111 |
+
|
| 112 |
+
return to_edge_transform_and_lower(
|
| 113 |
+
ep, partitioner=[WebGPUPartitioner()]
|
| 114 |
+
).to_executorch()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def lower_portable(ep):
|
| 118 |
+
from executorch.exir import to_edge_transform_and_lower
|
| 119 |
+
|
| 120 |
+
return to_edge_transform_and_lower(ep).to_executorch()
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def delegate_summary(pte_bytes):
|
| 124 |
+
"""Non-delegated kernels and per-delegate op histograms of a serialized program."""
|
| 125 |
+
import collections
|
| 126 |
+
|
| 127 |
+
from executorch.backends.vulkan.serialization.vulkan_graph_serialize import (
|
| 128 |
+
extract_vk_flatbuffer,
|
| 129 |
+
flatbuffer_to_vk_graph,
|
| 130 |
+
)
|
| 131 |
+
from executorch.exir._serialize._program import deserialize_pte_binary
|
| 132 |
+
|
| 133 |
+
pte = deserialize_pte_binary(pte_bytes)
|
| 134 |
+
prog = pte.program if hasattr(pte, "program") else pte
|
| 135 |
+
seg_base = int.from_bytes(pte_bytes[24:32], "little")
|
| 136 |
+
plan = prog.execution_plan[0]
|
| 137 |
+
ops = [f"{o.name}.{o.overload}" for o in plan.operators]
|
| 138 |
+
kernels = collections.Counter()
|
| 139 |
+
for chain in plan.chains:
|
| 140 |
+
for ins in chain.instructions:
|
| 141 |
+
if type(ins.instr_args).__name__ == "KernelCall":
|
| 142 |
+
kernels[ops[ins.instr_args.op_index]] += 1
|
| 143 |
+
delegates = []
|
| 144 |
+
for d in plan.delegates:
|
| 145 |
+
proc = d.processed
|
| 146 |
+
if proc.location.name == "INLINE":
|
| 147 |
+
blob = prog.backend_delegate_data[proc.index].data
|
| 148 |
+
else:
|
| 149 |
+
seg = prog.segments[proc.index]
|
| 150 |
+
blob = pte_bytes[seg_base + seg.offset : seg_base + seg.offset + seg.size]
|
| 151 |
+
g = flatbuffer_to_vk_graph(extract_vk_flatbuffer(blob))
|
| 152 |
+
delegates.append(
|
| 153 |
+
{
|
| 154 |
+
"id": d.id,
|
| 155 |
+
"ops": dict(collections.Counter(c.name for c in g.chain)),
|
| 156 |
+
"inputs": len(g.input_ids),
|
| 157 |
+
"outputs": len(g.output_ids),
|
| 158 |
+
}
|
| 159 |
+
)
|
| 160 |
+
return {"non_delegated_kernels": dict(kernels), "delegates": delegates}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def compare(a, b):
|
| 164 |
+
a = np.asarray(a, dtype=np.float64).ravel()
|
| 165 |
+
b = np.asarray(b, dtype=np.float64).ravel()
|
| 166 |
+
n = min(a.size, b.size)
|
| 167 |
+
a, b = a[:n], b[:n]
|
| 168 |
+
cos = float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-30))
|
| 169 |
+
return {
|
| 170 |
+
"max_abs": float(np.max(np.abs(a - b))),
|
| 171 |
+
"mean_abs": float(np.mean(np.abs(a - b))),
|
| 172 |
+
"cosine": cos,
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def run_pte(pte_bytes, inputs):
|
| 177 |
+
from executorch.extension.pybindings.portable_lib import (
|
| 178 |
+
_load_for_executorch_from_buffer,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
m = _load_for_executorch_from_buffer(pte_bytes)
|
| 182 |
+
return [
|
| 183 |
+
o.detach().numpy() if isinstance(o, torch.Tensor) else o
|
| 184 |
+
for o in m.forward(inputs)
|
| 185 |
+
]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def try_run_webgpu_pte_on_cpu(pte_bytes, inputs):
|
| 189 |
+
try:
|
| 190 |
+
return run_pte(pte_bytes, inputs), None
|
| 191 |
+
except Exception as e: # noqa: BLE001 - the error text is the result
|
| 192 |
+
return None, f"{type(e).__name__}: {str(e).splitlines()[0][:300]}"
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def export_vision(
|
| 196 |
+
*,
|
| 197 |
+
name,
|
| 198 |
+
model,
|
| 199 |
+
example_input,
|
| 200 |
+
out_dir,
|
| 201 |
+
upstream,
|
| 202 |
+
extra_recipe,
|
| 203 |
+
golden=None,
|
| 204 |
+
output_index=None,
|
| 205 |
+
):
|
| 206 |
+
"""Export, lower, write both .pte files, recipe.json, and validation.json.
|
| 207 |
+
|
| 208 |
+
`golden` is an optional (input ndarray, output ndarray) pair; when its input
|
| 209 |
+
shape matches `example_input`, all paths are also compared against it.
|
| 210 |
+
`output_index` picks one tensor when the eager model returns a tuple.
|
| 211 |
+
"""
|
| 212 |
+
out_dir = Path(out_dir)
|
| 213 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 214 |
+
torch.manual_seed(0)
|
| 215 |
+
model = model.eval()
|
| 216 |
+
|
| 217 |
+
class Wrapped(torch.nn.Module):
|
| 218 |
+
def __init__(self, m):
|
| 219 |
+
super().__init__()
|
| 220 |
+
self.m = m
|
| 221 |
+
|
| 222 |
+
def forward(self, x):
|
| 223 |
+
y = self.m(x)
|
| 224 |
+
return y[output_index] if output_index is not None else y
|
| 225 |
+
|
| 226 |
+
wrapped = Wrapped(model).eval()
|
| 227 |
+
with torch.no_grad():
|
| 228 |
+
ep = torch.export.export(wrapped, (example_input,), strict=False)
|
| 229 |
+
webgpu = lower_webgpu(ep)
|
| 230 |
+
portable = lower_portable(ep)
|
| 231 |
+
|
| 232 |
+
webgpu_path = out_dir / f"{name}_webgpu.pte"
|
| 233 |
+
portable_path = out_dir / f"{name}_portable.pte"
|
| 234 |
+
webgpu_path.write_bytes(webgpu.buffer)
|
| 235 |
+
portable_path.write_bytes(portable.buffer)
|
| 236 |
+
|
| 237 |
+
summary = delegate_summary(webgpu.buffer)
|
| 238 |
+
out_shape = list(ep.module()(example_input).shape)
|
| 239 |
+
recipe = {
|
| 240 |
+
"model": name,
|
| 241 |
+
"upstream": upstream,
|
| 242 |
+
"versions": versions(),
|
| 243 |
+
"export": {
|
| 244 |
+
"api": "torch.export.export(strict=False) -> to_edge_transform_and_lower(partitioner=[WebGPUPartitioner()]) -> to_executorch()",
|
| 245 |
+
"partitioner": "executorch.backends.webgpu.partitioner.WebGPUPartitioner() (default compile options; wraps VulkanPartitioner)",
|
| 246 |
+
"inputs": [
|
| 247 |
+
{
|
| 248 |
+
"shape": list(example_input.shape),
|
| 249 |
+
"dtype": "float32",
|
| 250 |
+
"layout": "NCHW",
|
| 251 |
+
}
|
| 252 |
+
],
|
| 253 |
+
"outputs": [{"shape": out_shape, "dtype": "float32"}],
|
| 254 |
+
"dtype": "float32",
|
| 255 |
+
"quantization": "none (fp32)",
|
| 256 |
+
**extra_recipe,
|
| 257 |
+
},
|
| 258 |
+
"files": {
|
| 259 |
+
webgpu_path.name: {
|
| 260 |
+
"bytes": webgpu_path.stat().st_size,
|
| 261 |
+
"sha256": sha256_file(webgpu_path),
|
| 262 |
+
},
|
| 263 |
+
portable_path.name: {
|
| 264 |
+
"bytes": portable_path.stat().st_size,
|
| 265 |
+
"sha256": sha256_file(portable_path),
|
| 266 |
+
"note": "CPU-only validation artifact (portable kernels, no delegate); not for release",
|
| 267 |
+
},
|
| 268 |
+
},
|
| 269 |
+
"graph": summary,
|
| 270 |
+
"command": command_line(out_dir),
|
| 271 |
+
}
|
| 272 |
+
(out_dir / "recipe.json").write_text(json.dumps(recipe, indent=2) + "\n")
|
| 273 |
+
|
| 274 |
+
# Validation on CPU.
|
| 275 |
+
gen = torch.Generator().manual_seed(1234)
|
| 276 |
+
rand_input = torch.rand(example_input.shape, generator=gen)
|
| 277 |
+
cases = {"random": rand_input}
|
| 278 |
+
if golden is not None and tuple(golden[0].shape) == tuple(example_input.shape):
|
| 279 |
+
cases["golden"] = torch.from_numpy(golden[0].copy())
|
| 280 |
+
report = {
|
| 281 |
+
"cpu_runtime": "executorch pybindings (portable_lib) from the pip wheel",
|
| 282 |
+
"cases": {},
|
| 283 |
+
}
|
| 284 |
+
webgpu_cpu_error = None
|
| 285 |
+
for label, x in cases.items():
|
| 286 |
+
with torch.no_grad():
|
| 287 |
+
eager = wrapped(x).numpy()
|
| 288 |
+
exported = ep.module()(x).numpy()
|
| 289 |
+
port = run_pte(portable.buffer, (x,))[0]
|
| 290 |
+
res = {
|
| 291 |
+
"exported_vs_eager": compare(exported, eager),
|
| 292 |
+
"portable_pte_vs_eager": compare(port, eager),
|
| 293 |
+
}
|
| 294 |
+
wg, err = try_run_webgpu_pte_on_cpu(webgpu.buffer, (x,))
|
| 295 |
+
if wg is not None:
|
| 296 |
+
res["webgpu_pte_vs_eager"] = compare(wg[0], eager)
|
| 297 |
+
else:
|
| 298 |
+
webgpu_cpu_error = err
|
| 299 |
+
if label == "golden":
|
| 300 |
+
ref = golden[1]
|
| 301 |
+
res["internal_golden_vs_eager"] = compare(eager, ref)
|
| 302 |
+
res["internal_golden_vs_portable_pte"] = compare(port, ref)
|
| 303 |
+
res["golden_note"] = (
|
| 304 |
+
"reference output from the internal goldens.manifest.json"
|
| 305 |
+
)
|
| 306 |
+
report["cases"][label] = res
|
| 307 |
+
report["webgpu_pte_on_cpu"] = (
|
| 308 |
+
"ran"
|
| 309 |
+
if webgpu_cpu_error is None
|
| 310 |
+
else f"not runnable on CPU pybindings: {webgpu_cpu_error}"
|
| 311 |
+
)
|
| 312 |
+
(out_dir / "validation.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 313 |
+
print(
|
| 314 |
+
json.dumps(
|
| 315 |
+
{"recipe": recipe["files"], "graph": summary, "validation": report},
|
| 316 |
+
indent=2,
|
| 317 |
+
)
|
| 318 |
+
)
|
| 319 |
+
return ep, webgpu, portable
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def load_golden(manifest_path, key):
|
| 323 |
+
man = json.loads(Path(manifest_path).read_text())[key]["tensors"]
|
| 324 |
+
base = Path(manifest_path).parent
|
| 325 |
+
|
| 326 |
+
def ld(t):
|
| 327 |
+
return np.fromfile(base / t["file"], dtype=np.float32).reshape(t["shape"])
|
| 328 |
+
|
| 329 |
+
inp = man.get("input")
|
| 330 |
+
return ld(inp), ld(man["output"])
|
export/_sam2.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Inspection helpers for the SAM 2 recipe (sam2-hiera-tiny.py). CPU only, read-only on inputs.
|
| 3 |
+
|
| 4 |
+
_sam2.py ops A.pte [B.pte] every delegated op call with its argument types/shapes; with B,
|
| 5 |
+
the op-signature set difference (ops/arity/ranks in A not in B)
|
| 6 |
+
_sam2.py consts A.pte B.pte how many delegate constants of A are byte-identical in B
|
| 7 |
+
_sam2.py io A.pte method names, program inputs/outputs, non-delegated kernels
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import collections
|
| 11 |
+
import hashlib
|
| 12 |
+
import json
|
| 13 |
+
import sys
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _program(data):
|
| 17 |
+
from executorch.exir._serialize._program import deserialize_pte_binary
|
| 18 |
+
|
| 19 |
+
pte = deserialize_pte_binary(data)
|
| 20 |
+
return pte, (pte.program if hasattr(pte, "program") else pte)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _blobs(data):
|
| 24 |
+
pte, prog = _program(data)
|
| 25 |
+
seg_base = int.from_bytes(data[24:32], "little")
|
| 26 |
+
out = []
|
| 27 |
+
for plan in prog.execution_plan:
|
| 28 |
+
for d in plan.delegates:
|
| 29 |
+
proc = d.processed
|
| 30 |
+
if proc.location.name == "INLINE":
|
| 31 |
+
out.append(
|
| 32 |
+
(
|
| 33 |
+
plan.name,
|
| 34 |
+
d.id,
|
| 35 |
+
bytes(prog.backend_delegate_data[proc.index].data),
|
| 36 |
+
)
|
| 37 |
+
)
|
| 38 |
+
else:
|
| 39 |
+
seg = prog.segments[proc.index]
|
| 40 |
+
out.append(
|
| 41 |
+
(
|
| 42 |
+
plan.name,
|
| 43 |
+
d.id,
|
| 44 |
+
data[seg_base + seg.offset : seg_base + seg.offset + seg.size],
|
| 45 |
+
)
|
| 46 |
+
)
|
| 47 |
+
nd = getattr(pte, "named_data", None)
|
| 48 |
+
resolve = (
|
| 49 |
+
(
|
| 50 |
+
lambda k: bytes(nd.buffers[nd.pte_data[k].buffer_index])
|
| 51 |
+
if k in nd.pte_data
|
| 52 |
+
else None
|
| 53 |
+
)
|
| 54 |
+
if nd
|
| 55 |
+
else (lambda k: None)
|
| 56 |
+
)
|
| 57 |
+
return out, resolve
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def vk_graph(blob):
|
| 61 |
+
from executorch.backends.vulkan.serialization.vulkan_graph_serialize import (
|
| 62 |
+
extract_vk_flatbuffer,
|
| 63 |
+
flatbuffer_to_vk_graph,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
return flatbuffer_to_vk_graph(extract_vk_flatbuffer(blob))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def describe(g, vid):
|
| 70 |
+
v = g.values[vid].value
|
| 71 |
+
n = type(v).__name__
|
| 72 |
+
if n == "VkTensor":
|
| 73 |
+
kind = "const" if v.constant_id >= 0 else "tensor"
|
| 74 |
+
return f"{kind}{list(v.dims)}:{v.datatype.name if hasattr(v.datatype, 'name') else v.datatype}"
|
| 75 |
+
if n == "Int":
|
| 76 |
+
return f"Int={v.int_val}"
|
| 77 |
+
if n == "Double":
|
| 78 |
+
return f"Double={v.double_val:g}"
|
| 79 |
+
if n == "Bool":
|
| 80 |
+
return f"Bool={v.bool_val}"
|
| 81 |
+
if n in ("IntList", "DoubleList", "BoolList"):
|
| 82 |
+
return f"{n}={list(v.items)}"
|
| 83 |
+
if n == "ValueList":
|
| 84 |
+
return "ValueList[" + ", ".join(describe(g, i) for i in v.items) + "]"
|
| 85 |
+
if n == "String":
|
| 86 |
+
return f"String={v.string_val!r}"
|
| 87 |
+
return n
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def ops(path):
|
| 91 |
+
data = open(path, "rb").read()
|
| 92 |
+
blobs, _ = _blobs(data)
|
| 93 |
+
calls = []
|
| 94 |
+
for plan, did, blob in blobs:
|
| 95 |
+
g = vk_graph(blob)
|
| 96 |
+
for c in g.chain:
|
| 97 |
+
calls.append(
|
| 98 |
+
{"plan": plan, "op": c.name, "args": [describe(g, a) for a in c.args]}
|
| 99 |
+
)
|
| 100 |
+
return calls
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def signature(call):
|
| 104 |
+
"""Op name + arity + per-argument kind/rank (shape-agnostic) for set comparisons."""
|
| 105 |
+
parts = []
|
| 106 |
+
for a in call["args"]:
|
| 107 |
+
if a.startswith(("tensor[", "const[")):
|
| 108 |
+
dims = json.loads(a[a.index("[") : a.index("]") + 1])
|
| 109 |
+
parts.append(f"{a.split('[')[0]}{len(dims)}d:{a.rsplit(':', 1)[1]}")
|
| 110 |
+
else:
|
| 111 |
+
parts.append(a.split("=")[0])
|
| 112 |
+
return f"{call['op']}({', '.join(parts)})"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def consts(path):
|
| 116 |
+
from executorch.backends.vulkan.serialization.vulkan_graph_serialize import (
|
| 117 |
+
VulkanDelegateHeader,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
data = open(path, "rb").read()
|
| 121 |
+
blobs, resolve = _blobs(data)
|
| 122 |
+
out = []
|
| 123 |
+
for plan, did, blob in blobs:
|
| 124 |
+
h = VulkanDelegateHeader.from_bytes(
|
| 125 |
+
blob[: VulkanDelegateHeader.EXPECTED_LENGTH]
|
| 126 |
+
)
|
| 127 |
+
g = vk_graph(blob)
|
| 128 |
+
raw = blob[h.bytes_offset : h.bytes_offset + h.bytes_size]
|
| 129 |
+
for v in g.values:
|
| 130 |
+
t = v.value
|
| 131 |
+
if type(t).__name__ != "VkTensor" or t.constant_id < 0:
|
| 132 |
+
continue
|
| 133 |
+
c = g.constants[t.constant_id]
|
| 134 |
+
b = (
|
| 135 |
+
resolve(c.named_key)
|
| 136 |
+
if c.named_key
|
| 137 |
+
else raw[c.offset : c.offset + c.length]
|
| 138 |
+
)
|
| 139 |
+
out.append(
|
| 140 |
+
{
|
| 141 |
+
"dims": list(t.dims),
|
| 142 |
+
"sha": hashlib.sha256(b).hexdigest(),
|
| 143 |
+
"bytes": len(b),
|
| 144 |
+
}
|
| 145 |
+
)
|
| 146 |
+
return out
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def io(path):
|
| 150 |
+
data = open(path, "rb").read()
|
| 151 |
+
_, prog = _program(data)
|
| 152 |
+
res = {"file": path, "bytes": len(data), "methods": {}}
|
| 153 |
+
for plan in prog.execution_plan:
|
| 154 |
+
|
| 155 |
+
def d(i):
|
| 156 |
+
v = plan.values[i].val
|
| 157 |
+
return (
|
| 158 |
+
f"{list(v.sizes)}:{v.scalar_type.name}"
|
| 159 |
+
if hasattr(v, "sizes")
|
| 160 |
+
else type(v).__name__
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
names = [f"{o.name}.{o.overload}" for o in plan.operators]
|
| 164 |
+
kernels = collections.Counter(
|
| 165 |
+
names[ins.instr_args.op_index]
|
| 166 |
+
for ch in plan.chains
|
| 167 |
+
for ins in ch.instructions
|
| 168 |
+
if type(ins.instr_args).__name__ == "KernelCall"
|
| 169 |
+
)
|
| 170 |
+
res["methods"][plan.name] = {
|
| 171 |
+
"inputs": [d(i) for i in plan.inputs],
|
| 172 |
+
"outputs": [d(i) for i in plan.outputs],
|
| 173 |
+
"delegates": [x.id for x in plan.delegates],
|
| 174 |
+
"non_delegated_kernels": dict(kernels),
|
| 175 |
+
}
|
| 176 |
+
return res
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
OFFICIAL = {
|
| 180 |
+
"sam2.1": ("facebook/sam2.1-hiera-tiny", "de431c4043854a71d8101e17995dfe596bf101a5", "sam2.1_hiera_tiny.pt",
|
| 181 |
+
"7402e0d864fa82708a20fbd15bc84245c2f26dff0eb43a4b5b93452deb34be69"),
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def verify_official(weights="sam2.1"):
|
| 186 |
+
import importlib.util
|
| 187 |
+
import pathlib
|
| 188 |
+
import tempfile
|
| 189 |
+
|
| 190 |
+
import torch
|
| 191 |
+
from huggingface_hub import hf_hub_download
|
| 192 |
+
from safetensors.torch import load_file
|
| 193 |
+
|
| 194 |
+
here = pathlib.Path(__file__).resolve().parent
|
| 195 |
+
spec = importlib.util.spec_from_file_location("sam2_recipe", here / "sam2-hiera-tiny.py")
|
| 196 |
+
recipe = importlib.util.module_from_spec(spec)
|
| 197 |
+
spec.loader.exec_module(recipe)
|
| 198 |
+
snap, digest = recipe.snapshot(weights)
|
| 199 |
+
hf = load_file(str(snap / "model.safetensors"))
|
| 200 |
+
repo, rev, name, want = OFFICIAL[weights]
|
| 201 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 202 |
+
p = hf_hub_download(repo, name, revision=rev, local_dir=tmp)
|
| 203 |
+
got = hashlib.sha256(open(p, "rb").read()).hexdigest()
|
| 204 |
+
if got != want:
|
| 205 |
+
sys.exit(f"{name}: sha256 {got} != {want}")
|
| 206 |
+
orig = torch.load(p, map_location="cpu", weights_only=True)["model"]
|
| 207 |
+
h = lambda t: hashlib.sha256(t.detach().contiguous().numpy().tobytes()).hexdigest() # noqa: E731
|
| 208 |
+
pool = {h(v): k for k, v in orig.items()}
|
| 209 |
+
matched, unmatched = 0, []
|
| 210 |
+
for k, v in hf.items():
|
| 211 |
+
if h(v) in pool:
|
| 212 |
+
matched += 1
|
| 213 |
+
else:
|
| 214 |
+
unmatched.append(k)
|
| 215 |
+
notes = {}
|
| 216 |
+
if "prompt_encoder.point_embed.weight" in unmatched:
|
| 217 |
+
pe = hf["prompt_encoder.point_embed.weight"]
|
| 218 |
+
if all(torch.equal(pe[i], orig[f"sam_prompt_encoder.point_embeddings.{i}.weight"][0]) for i in range(pe.shape[0])):
|
| 219 |
+
unmatched.remove("prompt_encoder.point_embed.weight")
|
| 220 |
+
matched += 1
|
| 221 |
+
notes["prompt_encoder.point_embed.weight"] = "rows == sam_prompt_encoder.point_embeddings.{0..3}.weight"
|
| 222 |
+
return {"hf": f"{repo}@{rev}/model.safetensors", "hf_sha256": digest, "official": f"{repo}@{rev}/{name}",
|
| 223 |
+
"official_sha256": want, "hf_tensors": len(hf), "official_tensors": len(orig),
|
| 224 |
+
"hf_tensors_bit_identical_to_official": matched, "unmatched": unmatched, "notes": notes}
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def main():
|
| 228 |
+
cmd, args = sys.argv[1], sys.argv[2:]
|
| 229 |
+
if cmd == "io":
|
| 230 |
+
print(json.dumps(io(args[0]), indent=1))
|
| 231 |
+
elif cmd == "ops":
|
| 232 |
+
a = ops(args[0])
|
| 233 |
+
if len(args) == 1:
|
| 234 |
+
for c in a:
|
| 235 |
+
print(c["op"], ", ".join(c["args"]))
|
| 236 |
+
return
|
| 237 |
+
b = ops(args[1])
|
| 238 |
+
sa, sb = (
|
| 239 |
+
collections.Counter(map(signature, a)),
|
| 240 |
+
collections.Counter(map(signature, b)),
|
| 241 |
+
)
|
| 242 |
+
print(
|
| 243 |
+
json.dumps(
|
| 244 |
+
{
|
| 245 |
+
"ops_count": [sum(sa.values()), sum(sb.values())],
|
| 246 |
+
"op_types_only_in_a": sorted(
|
| 247 |
+
{c["op"] for c in a} - {c["op"] for c in b}
|
| 248 |
+
),
|
| 249 |
+
"op_types_only_in_b": sorted(
|
| 250 |
+
{c["op"] for c in b} - {c["op"] for c in a}
|
| 251 |
+
),
|
| 252 |
+
"signatures_only_in_a": {
|
| 253 |
+
k: v for k, v in sorted(sa.items()) if k not in sb
|
| 254 |
+
},
|
| 255 |
+
"signatures_only_in_b": {
|
| 256 |
+
k: v for k, v in sorted(sb.items()) if k not in sa
|
| 257 |
+
},
|
| 258 |
+
},
|
| 259 |
+
indent=1,
|
| 260 |
+
)
|
| 261 |
+
)
|
| 262 |
+
elif cmd == "verify-official":
|
| 263 |
+
print(json.dumps(verify_official(*(args or ["sam2.1"])), indent=1))
|
| 264 |
+
elif cmd == "consts":
|
| 265 |
+
a, b = consts(args[0]), consts(args[1])
|
| 266 |
+
hb, ha = (
|
| 267 |
+
collections.Counter(c["sha"] for c in b),
|
| 268 |
+
collections.Counter(c["sha"] for c in a),
|
| 269 |
+
)
|
| 270 |
+
print(
|
| 271 |
+
json.dumps(
|
| 272 |
+
{
|
| 273 |
+
"a_constants": len(a),
|
| 274 |
+
"b_constants": len(b),
|
| 275 |
+
"a_found_in_b": sum(c["sha"] in hb for c in a),
|
| 276 |
+
"b_found_in_a": sum(c["sha"] in ha for c in b),
|
| 277 |
+
"a_missing": [c["dims"] for c in a if c["sha"] not in hb],
|
| 278 |
+
"b_missing": [c["dims"] for c in b if c["sha"] not in ha],
|
| 279 |
+
},
|
| 280 |
+
indent=1,
|
| 281 |
+
)
|
| 282 |
+
)
|
| 283 |
+
else:
|
| 284 |
+
sys.exit(__doc__)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
if __name__ == "__main__":
|
| 288 |
+
main()
|
export/recipe.json
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "sam2.1-hiera-tiny (512 encoder/decoder split)",
|
| 3 |
+
"upstream": {
|
| 4 |
+
"model": "facebook/sam2.1-hiera-tiny",
|
| 5 |
+
"revision": "de431c4043854a71d8101e17995dfe596bf101a5",
|
| 6 |
+
"official": true,
|
| 7 |
+
"license": "Apache-2.0",
|
| 8 |
+
"files": {
|
| 9 |
+
"model.safetensors": {
|
| 10 |
+
"sha256": "48c14467e5cf9e51870511feb72c89688e82dd74523142c0538b663e193ac2a7"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
"note": "transformers Sam2Model port published by the SAM 2 authors on the Hub"
|
| 14 |
+
},
|
| 15 |
+
"versions": {
|
| 16 |
+
"python": "3.12.15+meta",
|
| 17 |
+
"torch": "2.14.1+cpu",
|
| 18 |
+
"torchao": "0.18.0+cpu",
|
| 19 |
+
"executorch": "1.5.1+cpu",
|
| 20 |
+
"executorch_git": "3b60683923245cf472b7323426920e15623ba361",
|
| 21 |
+
"transformers": "5.19.0",
|
| 22 |
+
"huggingface_hub": "2.2.0"
|
| 23 |
+
},
|
| 24 |
+
"export": {
|
| 25 |
+
"api": "torch.export.export(strict=False) -> to_edge_transform_and_lower(partitioner=[WebGPUPartitioner()]) -> to_executorch(), one program per module",
|
| 26 |
+
"partitioner": "executorch.backends.webgpu.partitioner.WebGPUPartitioner() (default compile options; wraps VulkanPartitioner)",
|
| 27 |
+
"model_code": "transformers Sam2Model; attention through torch.ops.et_vk.sdpa (transformers attention backend, recipes/sam2-hiera-tiny.py:et_vk_attention); config image_size overridden 1024 -> 512",
|
| 28 |
+
"programs": {
|
| 29 |
+
"sam2_encoder.pte": {
|
| 30 |
+
"method": "forward",
|
| 31 |
+
"inputs": [
|
| 32 |
+
{
|
| 33 |
+
"name": "pixels",
|
| 34 |
+
"shape": [
|
| 35 |
+
1,
|
| 36 |
+
3,
|
| 37 |
+
512,
|
| 38 |
+
512
|
| 39 |
+
],
|
| 40 |
+
"dtype": "float32"
|
| 41 |
+
}
|
| 42 |
+
],
|
| 43 |
+
"outputs": [
|
| 44 |
+
{
|
| 45 |
+
"name": "image_embeddings",
|
| 46 |
+
"shape": [
|
| 47 |
+
1,
|
| 48 |
+
256,
|
| 49 |
+
32,
|
| 50 |
+
32
|
| 51 |
+
],
|
| 52 |
+
"dtype": "float32",
|
| 53 |
+
"note": "no_memory_embedding added"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"name": "hr0",
|
| 57 |
+
"shape": [
|
| 58 |
+
1,
|
| 59 |
+
256,
|
| 60 |
+
128,
|
| 61 |
+
128
|
| 62 |
+
],
|
| 63 |
+
"dtype": "float32",
|
| 64 |
+
"note": "before mask_decoder.conv_s0"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"name": "hr1",
|
| 68 |
+
"shape": [
|
| 69 |
+
1,
|
| 70 |
+
256,
|
| 71 |
+
64,
|
| 72 |
+
64
|
| 73 |
+
],
|
| 74 |
+
"dtype": "float32",
|
| 75 |
+
"note": "before mask_decoder.conv_s1"
|
| 76 |
+
}
|
| 77 |
+
]
|
| 78 |
+
},
|
| 79 |
+
"sam2_decoder.pte": {
|
| 80 |
+
"method": "forward",
|
| 81 |
+
"prompt_tokens": 3,
|
| 82 |
+
"inputs": [
|
| 83 |
+
{
|
| 84 |
+
"name": "image_embeddings",
|
| 85 |
+
"shape": [
|
| 86 |
+
1,
|
| 87 |
+
256,
|
| 88 |
+
32,
|
| 89 |
+
32
|
| 90 |
+
],
|
| 91 |
+
"dtype": "float32"
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"name": "image_pe",
|
| 95 |
+
"shape": [
|
| 96 |
+
1,
|
| 97 |
+
256,
|
| 98 |
+
32,
|
| 99 |
+
32
|
| 100 |
+
],
|
| 101 |
+
"dtype": "float32"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"name": "sparse",
|
| 105 |
+
"shape": [
|
| 106 |
+
1,
|
| 107 |
+
1,
|
| 108 |
+
3,
|
| 109 |
+
256
|
| 110 |
+
],
|
| 111 |
+
"dtype": "float32"
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"name": "dense",
|
| 115 |
+
"shape": [
|
| 116 |
+
1,
|
| 117 |
+
256,
|
| 118 |
+
32,
|
| 119 |
+
32
|
| 120 |
+
],
|
| 121 |
+
"dtype": "float32"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"name": "hr0",
|
| 125 |
+
"shape": [
|
| 126 |
+
1,
|
| 127 |
+
256,
|
| 128 |
+
128,
|
| 129 |
+
128
|
| 130 |
+
],
|
| 131 |
+
"dtype": "float32"
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"name": "hr1",
|
| 135 |
+
"shape": [
|
| 136 |
+
1,
|
| 137 |
+
256,
|
| 138 |
+
64,
|
| 139 |
+
64
|
| 140 |
+
],
|
| 141 |
+
"dtype": "float32"
|
| 142 |
+
}
|
| 143 |
+
],
|
| 144 |
+
"outputs": [
|
| 145 |
+
{
|
| 146 |
+
"name": "masks",
|
| 147 |
+
"shape": [
|
| 148 |
+
1,
|
| 149 |
+
3,
|
| 150 |
+
128,
|
| 151 |
+
128
|
| 152 |
+
],
|
| 153 |
+
"dtype": "float32",
|
| 154 |
+
"note": "logits, mask tokens 1..3"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"name": "iou_scores",
|
| 158 |
+
"shape": [
|
| 159 |
+
1,
|
| 160 |
+
1,
|
| 161 |
+
3
|
| 162 |
+
],
|
| 163 |
+
"dtype": "float32"
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"name": "object_score_logits",
|
| 167 |
+
"shape": [
|
| 168 |
+
1,
|
| 169 |
+
1,
|
| 170 |
+
1
|
| 171 |
+
],
|
| 172 |
+
"dtype": "float32"
|
| 173 |
+
}
|
| 174 |
+
]
|
| 175 |
+
}
|
| 176 |
+
},
|
| 177 |
+
"prompt_tables": {
|
| 178 |
+
"prompt_gaussian.bin": {
|
| 179 |
+
"shape": [
|
| 180 |
+
2,
|
| 181 |
+
128
|
| 182 |
+
],
|
| 183 |
+
"dtype": "float32 little-endian"
|
| 184 |
+
},
|
| 185 |
+
"prompt_point_embed_bg.bin": {
|
| 186 |
+
"shape": [
|
| 187 |
+
256
|
| 188 |
+
],
|
| 189 |
+
"dtype": "float32 little-endian"
|
| 190 |
+
},
|
| 191 |
+
"prompt_point_embed_fg.bin": {
|
| 192 |
+
"shape": [
|
| 193 |
+
256
|
| 194 |
+
],
|
| 195 |
+
"dtype": "float32 little-endian"
|
| 196 |
+
},
|
| 197 |
+
"prompt_box_tl.bin": {
|
| 198 |
+
"shape": [
|
| 199 |
+
256
|
| 200 |
+
],
|
| 201 |
+
"dtype": "float32 little-endian"
|
| 202 |
+
},
|
| 203 |
+
"prompt_box_br.bin": {
|
| 204 |
+
"shape": [
|
| 205 |
+
256
|
| 206 |
+
],
|
| 207 |
+
"dtype": "float32 little-endian"
|
| 208 |
+
},
|
| 209 |
+
"prompt_not_a_point.bin": {
|
| 210 |
+
"shape": [
|
| 211 |
+
256
|
| 212 |
+
],
|
| 213 |
+
"dtype": "float32 little-endian"
|
| 214 |
+
},
|
| 215 |
+
"prompt_img_pos.bin": {
|
| 216 |
+
"shape": [
|
| 217 |
+
1,
|
| 218 |
+
256,
|
| 219 |
+
32,
|
| 220 |
+
32
|
| 221 |
+
],
|
| 222 |
+
"dtype": "float32 little-endian"
|
| 223 |
+
},
|
| 224 |
+
"prompt_dense.bin": {
|
| 225 |
+
"shape": [
|
| 226 |
+
1,
|
| 227 |
+
256,
|
| 228 |
+
32,
|
| 229 |
+
32
|
| 230 |
+
],
|
| 231 |
+
"dtype": "float32 little-endian"
|
| 232 |
+
}
|
| 233 |
+
},
|
| 234 |
+
"prompt_table_sources": {
|
| 235 |
+
"prompt_gaussian.bin": "shared_image_embedding.positional_embedding",
|
| 236 |
+
"prompt_point_embed_bg.bin": "prompt_encoder.point_embed.weight[0]",
|
| 237 |
+
"prompt_point_embed_fg.bin": "prompt_encoder.point_embed.weight[1]",
|
| 238 |
+
"prompt_box_tl.bin": "prompt_encoder.point_embed.weight[2]",
|
| 239 |
+
"prompt_box_br.bin": "prompt_encoder.point_embed.weight[3]",
|
| 240 |
+
"prompt_not_a_point.bin": "prompt_encoder.not_a_point_embed.weight[0]",
|
| 241 |
+
"prompt_img_pos.bin": "Sam2Model.get_image_wide_positional_embeddings() with image_size=512",
|
| 242 |
+
"prompt_dense.bin": "prompt_encoder.no_mask_embed.weight broadcast to [1,256,32,32]"
|
| 243 |
+
},
|
| 244 |
+
"graph_rewrites": [
|
| 245 |
+
"Hiera q-pooling of windowed tensors [nW,h,w,C] done as one batch-1 max_pool2d over the windows stacked along H (exact: h is a multiple of the stride); keeps all pools in the delegate",
|
| 246 |
+
"attention via torch.ops.et_vk.sdpa (same math as torch SDPA; checked in validation.json)",
|
| 247 |
+
"decoder: a not-a-point token directly after another one gets an additive -1e9 key bias in every attention (computed in-graph from the sparse input), so padding up to the fixed token count does not change the result",
|
| 248 |
+
"Hiera position embedding (bicubic pos_embed resize + tiled window embedding) for the fixed input size computed in eager and stored as a constant"
|
| 249 |
+
],
|
| 250 |
+
"differences_from_upstream_pipeline": [
|
| 251 |
+
"input 512x512 instead of 1024x1024 (masks 128x128 instead of 256x256)",
|
| 252 |
+
"fixed 3 prompt tokens per decode: points, box corners, one not-a-point pad, then padding pads that the decoder masks out (exactly upstream's prompt for any prompt that fits; larger prompts are composed by the site)",
|
| 253 |
+
"no mask prompt input (dense = no_mask_embed)"
|
| 254 |
+
],
|
| 255 |
+
"preprocessing_outside_graph": "RGB, resize to 512x512 without keeping aspect, /255, ImageNet mean/std",
|
| 256 |
+
"dtype": "float32",
|
| 257 |
+
"quantization": "none (fp32)"
|
| 258 |
+
},
|
| 259 |
+
"files": {
|
| 260 |
+
"prompt_box_br.bin": {
|
| 261 |
+
"bytes": 1024,
|
| 262 |
+
"sha256": "a0df40a29af4a9b65d390ae46913d4170762a306fc8061ea87f1d8f4a4a1b0d5"
|
| 263 |
+
},
|
| 264 |
+
"prompt_box_tl.bin": {
|
| 265 |
+
"bytes": 1024,
|
| 266 |
+
"sha256": "96c208c44e95f7044bc480951be257c415cfa9c91db907e4575ae3c92d09e6b7"
|
| 267 |
+
},
|
| 268 |
+
"prompt_dense.bin": {
|
| 269 |
+
"bytes": 1048576,
|
| 270 |
+
"sha256": "52cece57b549dc81439c1156a07a25ff02db8cb81329e8eefb68c90457a884f3"
|
| 271 |
+
},
|
| 272 |
+
"prompt_gaussian.bin": {
|
| 273 |
+
"bytes": 1024,
|
| 274 |
+
"sha256": "c37a6a9361886286fc3e868dd279dfa4703919e3fee65c07a07e5a75e980d84f"
|
| 275 |
+
},
|
| 276 |
+
"prompt_img_pos.bin": {
|
| 277 |
+
"bytes": 1048576,
|
| 278 |
+
"sha256": "89dc4182ee4ac6a366dbfd0e9b825e4d7b6e88ded91b2ab06604eb5139e1cc2c"
|
| 279 |
+
},
|
| 280 |
+
"prompt_not_a_point.bin": {
|
| 281 |
+
"bytes": 1024,
|
| 282 |
+
"sha256": "19771c067b922d9d652e6f3dd1f6e2976c68abb8e6fd9db571255ae37687ea50"
|
| 283 |
+
},
|
| 284 |
+
"prompt_point_embed_bg.bin": {
|
| 285 |
+
"bytes": 1024,
|
| 286 |
+
"sha256": "91c432d10fcef8e32ce561eb4820beb6a772a61a3a937d7c2af8597061759cfa"
|
| 287 |
+
},
|
| 288 |
+
"prompt_point_embed_fg.bin": {
|
| 289 |
+
"bytes": 1024,
|
| 290 |
+
"sha256": "2e037dab5bee002d94bdb12d3141ab952cfe2315f0c9feb53a7c78a7e656ed33"
|
| 291 |
+
},
|
| 292 |
+
"sam2_decoder.pte": {
|
| 293 |
+
"bytes": 16949252,
|
| 294 |
+
"sha256": "57b91034acc71be2ce1835ea546a174295daee1046e40db0743e0238ef11e278"
|
| 295 |
+
},
|
| 296 |
+
"sam2_decoder_portable.pte": {
|
| 297 |
+
"bytes": 16968388,
|
| 298 |
+
"sha256": "cac19779cd445105fdfed5599bf4f6ed041fbc4d006c931be8bc2c3cdf3c4655",
|
| 299 |
+
"note": "CPU-only validation artifact (portable kernels, no delegate; torch SDPA instead of et_vk.sdpa); not for release"
|
| 300 |
+
},
|
| 301 |
+
"sam2_encoder.pte": {
|
| 302 |
+
"bytes": 115248512,
|
| 303 |
+
"sha256": "30f9fb469b779558b6b5c7c3eb995c50e1213ffbe082e31430c929ba2b3ffe20"
|
| 304 |
+
},
|
| 305 |
+
"sam2_encoder_portable.pte": {
|
| 306 |
+
"bytes": 115320192,
|
| 307 |
+
"sha256": "b18fc39b847470555f8b69401a2acc67a39f635124ce5ac9c2d6e25917fe44f7",
|
| 308 |
+
"note": "CPU-only validation artifact (portable kernels, no delegate; torch SDPA instead of et_vk.sdpa); not for release"
|
| 309 |
+
}
|
| 310 |
+
},
|
| 311 |
+
"graph": {
|
| 312 |
+
"sam2_encoder": {
|
| 313 |
+
"non_delegated_kernels": {},
|
| 314 |
+
"delegates": [
|
| 315 |
+
{
|
| 316 |
+
"id": "VulkanBackend",
|
| 317 |
+
"ops": {
|
| 318 |
+
"et_vk.prepack.default": 2,
|
| 319 |
+
"aten.convolution.default": 5,
|
| 320 |
+
"aten.clone.default": 32,
|
| 321 |
+
"aten.permute_copy.default": 83,
|
| 322 |
+
"aten.add.Tensor": 27,
|
| 323 |
+
"aten.native_layer_norm.default": 24,
|
| 324 |
+
"aten.constant_pad_nd.default": 9,
|
| 325 |
+
"aten.view_copy.default": 114,
|
| 326 |
+
"aten.linear.default": 51,
|
| 327 |
+
"aten.slice_copy.Tensor": 46,
|
| 328 |
+
"aten.squeeze_copy.dims": 36,
|
| 329 |
+
"et_vk.sdpa.default": 12,
|
| 330 |
+
"aten.gelu.default": 12,
|
| 331 |
+
"aten.max_pool2d_with_indices.default": 6,
|
| 332 |
+
"aten.upsample_nearest2d.vec": 1
|
| 333 |
+
},
|
| 334 |
+
"inputs": 1,
|
| 335 |
+
"outputs": 3
|
| 336 |
+
}
|
| 337 |
+
],
|
| 338 |
+
"io": {
|
| 339 |
+
"methods": [
|
| 340 |
+
"forward"
|
| 341 |
+
],
|
| 342 |
+
"inputs": [
|
| 343 |
+
{
|
| 344 |
+
"shape": [
|
| 345 |
+
1,
|
| 346 |
+
3,
|
| 347 |
+
512,
|
| 348 |
+
512
|
| 349 |
+
],
|
| 350 |
+
"dtype": "FLOAT",
|
| 351 |
+
"dim_order": [
|
| 352 |
+
0,
|
| 353 |
+
1,
|
| 354 |
+
2,
|
| 355 |
+
3
|
| 356 |
+
]
|
| 357 |
+
}
|
| 358 |
+
],
|
| 359 |
+
"outputs": [
|
| 360 |
+
{
|
| 361 |
+
"shape": [
|
| 362 |
+
1,
|
| 363 |
+
256,
|
| 364 |
+
32,
|
| 365 |
+
32
|
| 366 |
+
],
|
| 367 |
+
"dtype": "FLOAT",
|
| 368 |
+
"dim_order": [
|
| 369 |
+
0,
|
| 370 |
+
1,
|
| 371 |
+
2,
|
| 372 |
+
3
|
| 373 |
+
]
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"shape": [
|
| 377 |
+
1,
|
| 378 |
+
256,
|
| 379 |
+
128,
|
| 380 |
+
128
|
| 381 |
+
],
|
| 382 |
+
"dtype": "FLOAT",
|
| 383 |
+
"dim_order": [
|
| 384 |
+
0,
|
| 385 |
+
1,
|
| 386 |
+
2,
|
| 387 |
+
3
|
| 388 |
+
]
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"shape": [
|
| 392 |
+
1,
|
| 393 |
+
256,
|
| 394 |
+
64,
|
| 395 |
+
64
|
| 396 |
+
],
|
| 397 |
+
"dtype": "FLOAT",
|
| 398 |
+
"dim_order": [
|
| 399 |
+
0,
|
| 400 |
+
1,
|
| 401 |
+
2,
|
| 402 |
+
3
|
| 403 |
+
]
|
| 404 |
+
}
|
| 405 |
+
]
|
| 406 |
+
},
|
| 407 |
+
"io_contract": "matches the previous export (shapes, fp32, contiguous dim order, single forward method)"
|
| 408 |
+
},
|
| 409 |
+
"sam2_decoder": {
|
| 410 |
+
"non_delegated_kernels": {},
|
| 411 |
+
"delegates": [
|
| 412 |
+
{
|
| 413 |
+
"id": "VulkanBackend",
|
| 414 |
+
"ops": {
|
| 415 |
+
"et_vk.prepack.default": 13,
|
| 416 |
+
"aten.sub.Tensor": 2,
|
| 417 |
+
"aten.abs.default": 1,
|
| 418 |
+
"aten.sum.dim_IntList": 1,
|
| 419 |
+
"aten.mul.Tensor": 3,
|
| 420 |
+
"aten.relu.default": 15,
|
| 421 |
+
"aten.slice_copy.Tensor": 5,
|
| 422 |
+
"aten.full.default": 1,
|
| 423 |
+
"aten.clone.default": 23,
|
| 424 |
+
"aten.cat.default": 4,
|
| 425 |
+
"aten.unsqueeze_copy.default": 16,
|
| 426 |
+
"aten.convolution.default": 4,
|
| 427 |
+
"aten.repeat.default": 1,
|
| 428 |
+
"aten.add.Tensor": 25,
|
| 429 |
+
"aten.view_copy.default": 57,
|
| 430 |
+
"aten.permute_copy.default": 36,
|
| 431 |
+
"aten.squeeze_copy.dims": 34,
|
| 432 |
+
"aten.linear.default": 47,
|
| 433 |
+
"aten.expand_copy.default": 7,
|
| 434 |
+
"et_vk.sdpa.default": 7,
|
| 435 |
+
"aten.native_layer_norm.default": 10,
|
| 436 |
+
"aten.bmm.default": 4,
|
| 437 |
+
"aten.select_copy.int": 7,
|
| 438 |
+
"aten.gelu.default": 2,
|
| 439 |
+
"aten.sigmoid.default": 1
|
| 440 |
+
},
|
| 441 |
+
"inputs": 6,
|
| 442 |
+
"outputs": 3
|
| 443 |
+
}
|
| 444 |
+
],
|
| 445 |
+
"io": {
|
| 446 |
+
"methods": [
|
| 447 |
+
"forward"
|
| 448 |
+
],
|
| 449 |
+
"inputs": [
|
| 450 |
+
{
|
| 451 |
+
"shape": [
|
| 452 |
+
1,
|
| 453 |
+
256,
|
| 454 |
+
32,
|
| 455 |
+
32
|
| 456 |
+
],
|
| 457 |
+
"dtype": "FLOAT",
|
| 458 |
+
"dim_order": [
|
| 459 |
+
0,
|
| 460 |
+
1,
|
| 461 |
+
2,
|
| 462 |
+
3
|
| 463 |
+
]
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"shape": [
|
| 467 |
+
1,
|
| 468 |
+
256,
|
| 469 |
+
32,
|
| 470 |
+
32
|
| 471 |
+
],
|
| 472 |
+
"dtype": "FLOAT",
|
| 473 |
+
"dim_order": [
|
| 474 |
+
0,
|
| 475 |
+
1,
|
| 476 |
+
2,
|
| 477 |
+
3
|
| 478 |
+
]
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"shape": [
|
| 482 |
+
1,
|
| 483 |
+
1,
|
| 484 |
+
3,
|
| 485 |
+
256
|
| 486 |
+
],
|
| 487 |
+
"dtype": "FLOAT",
|
| 488 |
+
"dim_order": [
|
| 489 |
+
0,
|
| 490 |
+
1,
|
| 491 |
+
2,
|
| 492 |
+
3
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"shape": [
|
| 497 |
+
1,
|
| 498 |
+
256,
|
| 499 |
+
32,
|
| 500 |
+
32
|
| 501 |
+
],
|
| 502 |
+
"dtype": "FLOAT",
|
| 503 |
+
"dim_order": [
|
| 504 |
+
0,
|
| 505 |
+
1,
|
| 506 |
+
2,
|
| 507 |
+
3
|
| 508 |
+
]
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"shape": [
|
| 512 |
+
1,
|
| 513 |
+
256,
|
| 514 |
+
128,
|
| 515 |
+
128
|
| 516 |
+
],
|
| 517 |
+
"dtype": "FLOAT",
|
| 518 |
+
"dim_order": [
|
| 519 |
+
0,
|
| 520 |
+
1,
|
| 521 |
+
2,
|
| 522 |
+
3
|
| 523 |
+
]
|
| 524 |
+
},
|
| 525 |
+
{
|
| 526 |
+
"shape": [
|
| 527 |
+
1,
|
| 528 |
+
256,
|
| 529 |
+
64,
|
| 530 |
+
64
|
| 531 |
+
],
|
| 532 |
+
"dtype": "FLOAT",
|
| 533 |
+
"dim_order": [
|
| 534 |
+
0,
|
| 535 |
+
1,
|
| 536 |
+
2,
|
| 537 |
+
3
|
| 538 |
+
]
|
| 539 |
+
}
|
| 540 |
+
],
|
| 541 |
+
"outputs": [
|
| 542 |
+
{
|
| 543 |
+
"shape": [
|
| 544 |
+
1,
|
| 545 |
+
3,
|
| 546 |
+
128,
|
| 547 |
+
128
|
| 548 |
+
],
|
| 549 |
+
"dtype": "FLOAT",
|
| 550 |
+
"dim_order": [
|
| 551 |
+
0,
|
| 552 |
+
1,
|
| 553 |
+
2,
|
| 554 |
+
3
|
| 555 |
+
]
|
| 556 |
+
},
|
| 557 |
+
{
|
| 558 |
+
"shape": [
|
| 559 |
+
1,
|
| 560 |
+
1,
|
| 561 |
+
3
|
| 562 |
+
],
|
| 563 |
+
"dtype": "FLOAT",
|
| 564 |
+
"dim_order": [
|
| 565 |
+
0,
|
| 566 |
+
1,
|
| 567 |
+
2
|
| 568 |
+
]
|
| 569 |
+
},
|
| 570 |
+
{
|
| 571 |
+
"shape": [
|
| 572 |
+
1,
|
| 573 |
+
1,
|
| 574 |
+
1
|
| 575 |
+
],
|
| 576 |
+
"dtype": "FLOAT",
|
| 577 |
+
"dim_order": [
|
| 578 |
+
0,
|
| 579 |
+
1,
|
| 580 |
+
2
|
| 581 |
+
]
|
| 582 |
+
}
|
| 583 |
+
]
|
| 584 |
+
},
|
| 585 |
+
"io_contract": "matches the previous export (shapes, fp32, contiguous dim order, single forward method)"
|
| 586 |
+
}
|
| 587 |
+
},
|
| 588 |
+
"command": "python recipes/sam2-hiera-tiny.py --out out/sam2.1-hiera-tiny"
|
| 589 |
+
}
|
export/requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment the re-exports in REEXPORT.md were produced with (Python 3.12, Linux x86_64, CPU only).
|
| 2 |
+
# python -m venv venv
|
| 3 |
+
# venv/bin/pip install -r recipes/requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
|
| 4 |
+
# executorch 1.5.1 = pytorch/executorch git 3b60683923245cf472b7323426920e15623ba361; its wheel ships
|
| 5 |
+
# backends/webgpu (WebGPUPartitioner) and examples/models/llama (export_llm).
|
| 6 |
+
executorch==1.5.1
|
| 7 |
+
torch==2.14.1+cpu
|
| 8 |
+
torchvision==0.29.1+cpu
|
| 9 |
+
torchao==0.18.0+cpu
|
| 10 |
+
transformers==5.19.0
|
| 11 |
+
huggingface_hub==2.2.0
|
| 12 |
+
safetensors==0.8.0
|
| 13 |
+
tokenizers==0.23.3
|
| 14 |
+
numpy==2.5.4
|
| 15 |
+
pillow==12.3.0
|
export/sam2-hiera-tiny.py
ADDED
|
@@ -0,0 +1,939 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""SAM 2.1 Hiera Tiny -> two ExecuTorch WebGPU programs + prompt tables, 512x512 input.
|
| 3 |
+
|
| 4 |
+
Same split and I/O contract as the playground's previous export (sam2_encoder.pte, sam2_decoder.pte,
|
| 5 |
+
prompt_*.bin); the prompt encoder runs in JS (src/runtime/sam.js) from the tables.
|
| 6 |
+
|
| 7 |
+
sam2_encoder.pte forward(pixels[1,3,512,512] f32: RGB, square resize, /255, ImageNet mean/std)
|
| 8 |
+
-> (image_embeddings[1,256,32,32], hr0[1,256,128,128], hr1[1,256,64,64])
|
| 9 |
+
hr0/hr1 are the FPN levels *before* conv_s0/conv_s1 (the decoder applies them).
|
| 10 |
+
sam2_decoder.pte forward(image_embeddings[1,256,32,32], image_pe[1,256,32,32], sparse[1,1,3,256],
|
| 11 |
+
dense[1,256,32,32], hr0[1,256,128,128], hr1[1,256,64,64])
|
| 12 |
+
-> (masks[1,3,128,128] logits, iou_scores[1,1,3], object_score_logits[1,1,1])
|
| 13 |
+
multimask output (mask tokens 1..3), three prompt tokens (--prompt-tokens): points,
|
| 14 |
+
box corners, one not-a-point pad, then pads; pads after the first are masked, so
|
| 15 |
+
clicks, click+exclude and boxes decode exactly as upstream.
|
| 16 |
+
prompt_gaussian.bin [2,128] f32 random-Fourier matrix (shared_image_embedding)
|
| 17 |
+
prompt_point_embed_bg.bin [256] f32 point_embed[0] (negative click)
|
| 18 |
+
prompt_point_embed_fg.bin [256] f32 point_embed[1] (positive click)
|
| 19 |
+
prompt_box_tl.bin / _br.bin [256] f32 point_embed[2] / [3] (box corners)
|
| 20 |
+
prompt_not_a_point.bin [256] f32 not_a_point_embed (padding token)
|
| 21 |
+
prompt_img_pos.bin [1,256,32,32] f32 get_image_wide_positional_embeddings() at 512
|
| 22 |
+
prompt_dense.bin [1,256,32,32] f32 no_mask_embed broadcast (no mask prompt)
|
| 23 |
+
|
| 24 |
+
Upstream: the HF transformers port (Apache-2.0) of facebook/sam2.1-hiera-tiny, which is what the
|
| 25 |
+
previous export was built from (its decoder weights and all prompt tables are bit-identical to it).
|
| 26 |
+
`--weights sam2` uses facebook/sam2-hiera-tiny (SAM 2.0) instead.
|
| 27 |
+
|
| 28 |
+
Graph changes, all output-preserving: attention goes through the public fused op
|
| 29 |
+
torch.ops.et_vk.sdpa (registered as a transformers attention backend); the Hiera windowed absolute
|
| 30 |
+
position embedding (bicubic resize of pos_embed + tiled window embedding) for the fixed input size is
|
| 31 |
+
computed once in eager and stored as a constant; Hiera's q-pooling of windowed tensors runs as one
|
| 32 |
+
batch-1 max_pool2d over the windows stacked along H (exact; keeps every pool in the delegate). Unlike the previous export, the encoder adds
|
| 33 |
+
`no_memory_embedding` to the 32x32 embedding, as upstream does for single images
|
| 34 |
+
(transformers Sam2Model.forward; `directly_add_no_mem_embed: true` in the official yaml).
|
| 35 |
+
`--no-memory-embedding omit` reproduces the previous behaviour.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
import argparse
|
| 39 |
+
import json
|
| 40 |
+
import sys
|
| 41 |
+
import types
|
| 42 |
+
from pathlib import Path
|
| 43 |
+
|
| 44 |
+
import numpy as np
|
| 45 |
+
import torch
|
| 46 |
+
import torch.nn.functional as F
|
| 47 |
+
|
| 48 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 49 |
+
import _common # noqa: E402
|
| 50 |
+
|
| 51 |
+
WEIGHTS = {
|
| 52 |
+
"sam2.1": {
|
| 53 |
+
"repo": "facebook/sam2.1-hiera-tiny",
|
| 54 |
+
"revision": "de431c4043854a71d8101e17995dfe596bf101a5",
|
| 55 |
+
"files": {
|
| 56 |
+
"model.safetensors": "48c14467e5cf9e51870511feb72c89688e82dd74523142c0538b663e193ac2a7",
|
| 57 |
+
},
|
| 58 |
+
},
|
| 59 |
+
"sam2": {
|
| 60 |
+
"repo": "facebook/sam2-hiera-tiny",
|
| 61 |
+
"revision": "7c218beaf0bb87874785f32b582f640134fc1c09",
|
| 62 |
+
"files": {
|
| 63 |
+
"model.safetensors": "1449035ba6ba2ed524c646da989e5cb3a9b93b97f1c1b2d474e598c3b748d99f",
|
| 64 |
+
},
|
| 65 |
+
},
|
| 66 |
+
}
|
| 67 |
+
CONFIG_FILES = ("config.json", "preprocessor_config.json", "processor_config.json")
|
| 68 |
+
|
| 69 |
+
# Validation images. Prompts are in original-image pixels, without padding; site_tokens() turns
|
| 70 |
+
# them into the decoder's tokens the way planPrompt() in sam.js does (points, box corners, one
|
| 71 |
+
# not-a-point pad, then pads up to the decoder's token count).
|
| 72 |
+
IMAGE = {
|
| 73 |
+
"name": "sam-car.png",
|
| 74 |
+
"repo": "huggingface/documentation-images",
|
| 75 |
+
"repo_type": "dataset",
|
| 76 |
+
"revision": "79244b5169b24b4e1444393d9b87349c9182cfa4",
|
| 77 |
+
"file": "transformers/model_doc/sam-car.png",
|
| 78 |
+
"sha256": "c6ae6440e3862dd9768cd9f7098b783ac980629dd7bc1fe69c10b39fca5a84cd",
|
| 79 |
+
"prompts": {
|
| 80 |
+
"door_click": [("pos", 1680, 840)],
|
| 81 |
+
"wheel_click": [("pos", 832, 1088)],
|
| 82 |
+
"door_plus_exclude": [("pos", 1680, 840), ("neg", 1000, 200)],
|
| 83 |
+
"car_box": [("tl", 120, 400), ("br", 2528, 1248)],
|
| 84 |
+
"door_and_wheel_clicks": [("pos", 1680, 840), ("pos", 832, 1088)],
|
| 85 |
+
},
|
| 86 |
+
}
|
| 87 |
+
# Optional: extra sample images for validation (`--assets-dir`).
|
| 88 |
+
# Not public inputs; pinned by sha256 so the numbers are reproducible.
|
| 89 |
+
ASSET_IMAGES = {
|
| 90 |
+
"real_pets.jpg": {
|
| 91 |
+
"sha256": "85e4f2f27368a203c30eac68a71e4dde40edac6417b4a8c9df80af5d30b191b2",
|
| 92 |
+
"prompts": {
|
| 93 |
+
"pug_face_click": [("pos", 505, 150)],
|
| 94 |
+
"blanket_click": [("pos", 500, 500)],
|
| 95 |
+
"blanket_minus_face": [("pos", 500, 500), ("neg", 505, 150)],
|
| 96 |
+
"pug_box": [("tl", 165, 65), ("br", 850, 720)],
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
"yolo_people.jpg": {
|
| 100 |
+
"sha256": "16d73869e3267a7d4ed00de8e860833bd1657c1b252e94c0c348277adc7b6edb",
|
| 101 |
+
"prompts": {
|
| 102 |
+
"left_man_head_click": [("pos", 570, 260)],
|
| 103 |
+
"right_man_face_click": [("pos", 980, 180)],
|
| 104 |
+
"left_jacket_minus_right": [("pos", 300, 520), ("neg", 880, 520)],
|
| 105 |
+
"right_man_box": [("tl", 750, 40), ("br", 1150, 720)],
|
| 106 |
+
},
|
| 107 |
+
},
|
| 108 |
+
}
|
| 109 |
+
SIZE = 512
|
| 110 |
+
MEAN = (0.485, 0.456, 0.406)
|
| 111 |
+
STD = (0.229, 0.224, 0.225)
|
| 112 |
+
INPUT_NAMES = ["image_embeddings", "image_pe", "sparse", "dense", "hr0", "hr1"]
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# Additive key bias [1, 1, 1, tokens] set by Decoder.forward while the mask decoder runs: it hides
|
| 116 |
+
# extra padding tokens (see Decoder.pad_bias). None everywhere else (encoder, references).
|
| 117 |
+
_TOKEN_KEY_BIAS = None
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _key_bias(query, key, attention_mask):
|
| 121 |
+
bias = _TOKEN_KEY_BIAS
|
| 122 |
+
if bias is None or key.shape[-2] != bias.shape[-1]:
|
| 123 |
+
return attention_mask
|
| 124 |
+
full = bias.expand(query.shape[0], query.shape[1], query.shape[2], key.shape[-2])
|
| 125 |
+
return full if attention_mask is None else attention_mask + full
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def et_vk_attention(
|
| 129 |
+
module, query, key, value, attention_mask, scaling=None, dropout=0.0, **kwargs
|
| 130 |
+
):
|
| 131 |
+
"""transformers attention backend emitting the fused et_vk.sdpa op (public ExecuTorch Vulkan custom op,
|
| 132 |
+
registered by the WebGPU runtime). q/k/v are [B, H, S, D]; returns [B, S, H, D] like the built-in backends."""
|
| 133 |
+
import executorch.backends.vulkan.custom_ops_lib # noqa: F401 (defines torch.ops.et_vk.*)
|
| 134 |
+
|
| 135 |
+
mask = _key_bias(query, key, attention_mask)
|
| 136 |
+
out = torch.ops.et_vk.sdpa(query, key, value, mask, scaling)
|
| 137 |
+
return out.transpose(1, 2).contiguous(), None
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def torch_attention(
|
| 141 |
+
module, query, key, value, attention_mask, scaling=None, dropout=0.0, **kwargs
|
| 142 |
+
):
|
| 143 |
+
"""Same as et_vk_attention with torch SDPA (for the CPU-only portable decoder)."""
|
| 144 |
+
mask = _key_bias(query, key, attention_mask)
|
| 145 |
+
out = F.scaled_dot_product_attention(query, key, value, attn_mask=mask, scale=scaling)
|
| 146 |
+
return out.transpose(1, 2).contiguous(), None
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def register_attention():
|
| 150 |
+
from transformers import AttentionInterface
|
| 151 |
+
|
| 152 |
+
AttentionInterface.register("et_vk_sdpa", et_vk_attention)
|
| 153 |
+
AttentionInterface.register("torch_sdpa_padmask", torch_attention)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def site_tokens(prompt, n_tokens):
|
| 157 |
+
"""planPrompt() in sam.js for a prompt that fits one decode: points (positive first), box corners,
|
| 158 |
+
one not-a-point pad (SAM 2's own padding), then pads up to the decoder's token count."""
|
| 159 |
+
pos = [t for t in prompt if t[0] == "pos"]
|
| 160 |
+
neg = [t for t in prompt if t[0] == "neg"]
|
| 161 |
+
corners = [t for t in prompt if t[0] in ("tl", "br")]
|
| 162 |
+
native = pos + neg + corners + [("pad",)]
|
| 163 |
+
if len(native) > n_tokens:
|
| 164 |
+
sys.exit(f"prompt {prompt} needs {len(native)} tokens; the decoder takes {n_tokens}")
|
| 165 |
+
return native + [("pad",)] * (n_tokens - len(native))
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def snapshot(weights):
|
| 169 |
+
"""Pinned upstream snapshot: weights (sha256-checked) + configs at the same revision."""
|
| 170 |
+
w = WEIGHTS[weights]
|
| 171 |
+
path, digest = _common.hf_file(
|
| 172 |
+
w["repo"], "model.safetensors", w["revision"], w["files"]["model.safetensors"]
|
| 173 |
+
)
|
| 174 |
+
for f in CONFIG_FILES:
|
| 175 |
+
_common.hf_file(w["repo"], f, w["revision"])
|
| 176 |
+
return path.parent, digest
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def load_model(snap, size, attn="sdpa"):
|
| 180 |
+
from transformers import Sam2Config, Sam2Model
|
| 181 |
+
|
| 182 |
+
cfg = Sam2Config.from_pretrained(snap)
|
| 183 |
+
if size != cfg.image_size:
|
| 184 |
+
f = size // cfg.prompt_encoder_config.patch_size
|
| 185 |
+
cfg.image_size = size
|
| 186 |
+
cfg.prompt_encoder_config.image_size = size
|
| 187 |
+
cfg.vision_config.backbone_config.image_size = [size, size]
|
| 188 |
+
cfg.vision_config.backbone_feature_sizes = [
|
| 189 |
+
[4 * f, 4 * f],
|
| 190 |
+
[2 * f, 2 * f],
|
| 191 |
+
[f, f],
|
| 192 |
+
]
|
| 193 |
+
model, info = Sam2Model.from_pretrained(
|
| 194 |
+
snap, config=cfg, attn_implementation=attn, output_loading_info=True
|
| 195 |
+
)
|
| 196 |
+
if info["missing_keys"] or info["mismatched_keys"] or info["unexpected_keys"]:
|
| 197 |
+
sys.exit(f"weights did not load cleanly: {info}")
|
| 198 |
+
return model.eval()
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def pool_windows_as_one_image():
|
| 202 |
+
"""Hiera's q-pooling (2x2 max pool, stride 2) on windowed tensors [nW, h, w, C] is rewritten to pool
|
| 203 |
+
the windows stacked along H as a single [1, C, nW*h, w] image. h is a multiple of the stride, so no
|
| 204 |
+
pooling cell straddles two windows and the result is identical; batch-1 pooling is what the Vulkan
|
| 205 |
+
partitioner can represent (a [256, 384, 4, 4] pool is left on CPU otherwise)."""
|
| 206 |
+
from transformers.models.sam2 import modeling_sam2
|
| 207 |
+
|
| 208 |
+
original = getattr(modeling_sam2, "_upstream_do_pool", modeling_sam2.do_pool)
|
| 209 |
+
modeling_sam2._upstream_do_pool = original
|
| 210 |
+
|
| 211 |
+
def do_pool(x, query_stride=None):
|
| 212 |
+
if query_stride is None or x.shape[0] == 1:
|
| 213 |
+
return original(x, query_stride)
|
| 214 |
+
b, h, w, c = x.shape
|
| 215 |
+
sh, sw = (query_stride, query_stride) if isinstance(query_stride, int) else tuple(query_stride)
|
| 216 |
+
if h % sh or w % sw:
|
| 217 |
+
return original(x, query_stride)
|
| 218 |
+
y = x.reshape(1, b * h, w, c).permute(0, 3, 1, 2)
|
| 219 |
+
y = F.max_pool2d(y, kernel_size=query_stride, stride=query_stride, ceil_mode=False)
|
| 220 |
+
return y.permute(0, 2, 3, 1).reshape(b, h // sh, w // sw, c)
|
| 221 |
+
|
| 222 |
+
modeling_sam2.do_pool = do_pool
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def upstream_pool():
|
| 226 |
+
"""Undo pool_windows_as_one_image() (upstream code path, for references)."""
|
| 227 |
+
from transformers.models.sam2 import modeling_sam2
|
| 228 |
+
|
| 229 |
+
modeling_sam2.do_pool = getattr(modeling_sam2, "_upstream_do_pool", modeling_sam2.do_pool)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def freeze_hiera_pos_embed(model, size):
|
| 233 |
+
bb = model.vision_encoder.backbone
|
| 234 |
+
with torch.no_grad():
|
| 235 |
+
frozen = bb._get_pos_embed((size // 4, size // 4)).detach().contiguous().clone()
|
| 236 |
+
bb.register_buffer("frozen_pos_embed", frozen, persistent=False)
|
| 237 |
+
bb._get_pos_embed = types.MethodType(lambda self, _hw: self.frozen_pos_embed, bb)
|
| 238 |
+
return model
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class Encoder(torch.nn.Module):
|
| 242 |
+
def __init__(self, model, add_no_memory=True):
|
| 243 |
+
super().__init__()
|
| 244 |
+
self.vision = model.vision_encoder
|
| 245 |
+
self.add_no_memory = add_no_memory
|
| 246 |
+
self.register_buffer(
|
| 247 |
+
"no_mem",
|
| 248 |
+
model.no_memory_embedding.detach().reshape(1, -1, 1, 1).clone(),
|
| 249 |
+
persistent=False,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def forward(self, pixels):
|
| 253 |
+
hr0, hr1, emb = self.vision(pixels).fpn_hidden_states
|
| 254 |
+
if self.add_no_memory:
|
| 255 |
+
emb = emb + self.no_mem
|
| 256 |
+
return emb, hr0, hr1
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class Decoder(torch.nn.Module):
|
| 260 |
+
"""Mask decoder on precomputed prompt embeddings.
|
| 261 |
+
|
| 262 |
+
A pad (not-a-point) token that directly follows another pad is hidden from every attention as a
|
| 263 |
+
key (additive -1e9, which underflows to an exact 0 weight). Nothing reads such a token's own
|
| 264 |
+
output, so a fixed-size decode of [pos, pad, pad] equals upstream's [pos, pad], and [tl, br, pad]
|
| 265 |
+
equals upstream's box prompt. The first pad is kept: it is SAM 2's own padding token."""
|
| 266 |
+
|
| 267 |
+
def __init__(self, model):
|
| 268 |
+
super().__init__()
|
| 269 |
+
self.md = model.mask_decoder
|
| 270 |
+
self.n_out = self.md.obj_score_token.weight.shape[0] + 1 + self.md.num_mask_tokens
|
| 271 |
+
self.register_buffer(
|
| 272 |
+
"not_a_point",
|
| 273 |
+
model.prompt_encoder.not_a_point_embed.weight[0].detach().clone(),
|
| 274 |
+
persistent=False,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
def pad_bias(self, sparse):
|
| 278 |
+
d = (sparse - self.not_a_point).abs().sum(-1) # [1, 1, N]; 0 only for an exact pad token
|
| 279 |
+
is_pad = torch.relu(1.0 - d * 1e6) # 1.0 for a pad, 0.0 otherwise
|
| 280 |
+
repeated = is_pad[..., 1:] * is_pad[..., :-1]
|
| 281 |
+
keep = torch.zeros(1, 1, self.n_out + 1, dtype=sparse.dtype)
|
| 282 |
+
return (torch.cat([keep, repeated], dim=-1) * -1e9).unsqueeze(2) # [1, 1, 1, n_out + N]
|
| 283 |
+
|
| 284 |
+
def forward(self, emb, image_pe, sparse, dense, hr0, hr1):
|
| 285 |
+
global _TOKEN_KEY_BIAS
|
| 286 |
+
_TOKEN_KEY_BIAS = self.pad_bias(sparse) if sparse.shape[2] > 1 else None
|
| 287 |
+
try:
|
| 288 |
+
return self._decode(emb, image_pe, sparse, dense, hr0, hr1)
|
| 289 |
+
finally:
|
| 290 |
+
_TOKEN_KEY_BIAS = None
|
| 291 |
+
|
| 292 |
+
def _decode(self, emb, image_pe, sparse, dense, hr0, hr1):
|
| 293 |
+
masks, iou, _, obj = self.md(
|
| 294 |
+
image_embeddings=emb,
|
| 295 |
+
image_positional_embeddings=image_pe,
|
| 296 |
+
sparse_prompt_embeddings=sparse,
|
| 297 |
+
dense_prompt_embeddings=dense,
|
| 298 |
+
multimask_output=True,
|
| 299 |
+
high_resolution_features=[self.md.conv_s0(hr0), self.md.conv_s1(hr1)],
|
| 300 |
+
)
|
| 301 |
+
# [B, point_batch=1, 3, H, W] -> [B, 3, H, W], as the previous export.
|
| 302 |
+
return masks[:, 0], iou, obj
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def prompt_tables(model):
|
| 306 |
+
pe = model.prompt_encoder
|
| 307 |
+
with torch.no_grad():
|
| 308 |
+
dense = (
|
| 309 |
+
pe.no_mask_embed.weight.reshape(1, -1, 1, 1)
|
| 310 |
+
.expand(1, -1, *pe.image_embedding_size)
|
| 311 |
+
.contiguous()
|
| 312 |
+
)
|
| 313 |
+
tabs = {
|
| 314 |
+
"gaussian": model.shared_image_embedding.positional_embedding,
|
| 315 |
+
"point_embed_bg": pe.point_embed.weight[0],
|
| 316 |
+
"point_embed_fg": pe.point_embed.weight[1],
|
| 317 |
+
"box_tl": pe.point_embed.weight[2],
|
| 318 |
+
"box_br": pe.point_embed.weight[3],
|
| 319 |
+
"not_a_point": pe.not_a_point_embed.weight[0],
|
| 320 |
+
"img_pos": model.get_image_wide_positional_embeddings(),
|
| 321 |
+
"dense": dense,
|
| 322 |
+
}
|
| 323 |
+
return {
|
| 324 |
+
k: v.detach().to(torch.float32).contiguous().numpy() for k, v in tabs.items()
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def encode_tokens(tokens, width, height, tabs, size=SIZE):
|
| 329 |
+
"""Python port of the playground's browser-side prompt encoding (float64 math, f32 out)."""
|
| 330 |
+
g = tabs["gaussian"].reshape(-1).astype(np.float64)
|
| 331 |
+
hidden = tabs["not_a_point"].size
|
| 332 |
+
D = hidden // 2
|
| 333 |
+
kind = {
|
| 334 |
+
"neg": tabs["point_embed_bg"],
|
| 335 |
+
"pos": tabs["point_embed_fg"],
|
| 336 |
+
"tl": tabs["box_tl"],
|
| 337 |
+
"br": tabs["box_br"],
|
| 338 |
+
}
|
| 339 |
+
out = np.zeros((len(tokens), hidden), np.float64)
|
| 340 |
+
for r, t in enumerate(tokens):
|
| 341 |
+
if t[0] == "pad":
|
| 342 |
+
out[r] = tabs["not_a_point"]
|
| 343 |
+
continue
|
| 344 |
+
x, y = round(t[1] * 10) / 10, round(t[2] * 10) / 10
|
| 345 |
+
cx = (2 * (x * (size / width) + 0.5)) / size - 1
|
| 346 |
+
cy = (2 * (y * (size / height) + 0.5)) / size - 1
|
| 347 |
+
a = 2 * np.pi * (cx * g[:D] + cy * g[D:])
|
| 348 |
+
out[r, :D] = np.sin(a) + kind[t[0]][:D]
|
| 349 |
+
out[r, D:] = np.cos(a) + kind[t[0]][D:]
|
| 350 |
+
return torch.from_numpy(out.astype(np.float32)).reshape(1, 1, len(tokens), hidden)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def load_image(path, size):
|
| 354 |
+
from PIL import Image
|
| 355 |
+
|
| 356 |
+
img = Image.open(path).convert("RGB")
|
| 357 |
+
a = np.asarray(img.resize((size, size), Image.BILINEAR), dtype=np.float32) / 255.0
|
| 358 |
+
a = (a - np.array(MEAN, np.float32)) / np.array(STD, np.float32)
|
| 359 |
+
return torch.from_numpy(a.transpose(2, 0, 1).copy()).unsqueeze(0), img
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def iou(a, b):
|
| 363 |
+
a, b = np.asarray(a) > 0, np.asarray(b) > 0
|
| 364 |
+
u = np.logical_or(a, b).sum()
|
| 365 |
+
return round(float(np.logical_and(a, b).sum() / u), 4) if u else 1.0
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def upsample(masks, w, h):
|
| 369 |
+
"""Bilinear, align_corners=False: the same mapping as upsampleMask() in sam.js."""
|
| 370 |
+
t = torch.from_numpy(np.ascontiguousarray(masks, np.float32)).reshape(
|
| 371 |
+
1, -1, *np.asarray(masks).shape[-2:]
|
| 372 |
+
)
|
| 373 |
+
return F.interpolate(t, size=(h, w), mode="bilinear", align_corners=False)[
|
| 374 |
+
0
|
| 375 |
+
].numpy()
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def upstream_inputs(tokens):
|
| 379 |
+
"""The same prompt in transformers' native form (Sam2PromptEncoder pads points with a not-a-point
|
| 380 |
+
token and always adds one after box corners, like the original SAM 2 predictor)."""
|
| 381 |
+
pts = [t for t in tokens if t[0] in ("pos", "neg")]
|
| 382 |
+
corners = [t for t in tokens if t[0] in ("tl", "br")]
|
| 383 |
+
kw = {}
|
| 384 |
+
if corners:
|
| 385 |
+
(_, x0, y0), (_, x1, y1) = corners
|
| 386 |
+
kw["input_boxes"] = [[[x0, y0, x1, y1]]]
|
| 387 |
+
if pts:
|
| 388 |
+
kw["input_points"] = [[[[t[1], t[2]] for t in pts]]]
|
| 389 |
+
kw["input_labels"] = [[[1 if t[0] == "pos" else 0 for t in pts]]]
|
| 390 |
+
return kw
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def upstream_reference(model, processor, img, tokens):
|
| 394 |
+
"""Upstream transformers pipeline end to end (Sam2Processor -> Sam2Model -> post_process_masks):
|
| 395 |
+
mask logits at the original image size, and the IoU head's scores."""
|
| 396 |
+
inputs = processor(images=img, return_tensors="pt", **upstream_inputs(tokens))
|
| 397 |
+
with torch.no_grad():
|
| 398 |
+
o = model(
|
| 399 |
+
pixel_values=inputs["pixel_values"],
|
| 400 |
+
input_points=inputs.get("input_points"),
|
| 401 |
+
input_labels=inputs.get("input_labels"),
|
| 402 |
+
input_boxes=inputs.get("input_boxes"),
|
| 403 |
+
multimask_output=True,
|
| 404 |
+
)
|
| 405 |
+
m = processor.post_process_masks(
|
| 406 |
+
o.pred_masks, inputs["original_sizes"], binarize=False
|
| 407 |
+
)[0]
|
| 408 |
+
return m[0].numpy(), o.iou_scores[0, 0].numpy()
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def upstream_512_same_pixels(model, pixels, tokens, w, h):
|
| 412 |
+
"""Upstream Sam2Model.forward (512 config) on exactly the pixels the export sees."""
|
| 413 |
+
s = model.prompt_encoder.input_image_size
|
| 414 |
+
kw = upstream_inputs(tokens)
|
| 415 |
+
sx, sy = s / w, s / h
|
| 416 |
+
args = {}
|
| 417 |
+
if "input_boxes" in kw:
|
| 418 |
+
x0, y0, x1, y1 = kw["input_boxes"][0][0]
|
| 419 |
+
args["input_boxes"] = torch.tensor(
|
| 420 |
+
[[[x0 * sx, y0 * sy, x1 * sx, y1 * sy]]], dtype=torch.float32
|
| 421 |
+
)
|
| 422 |
+
if "input_points" in kw:
|
| 423 |
+
args["input_points"] = torch.tensor(
|
| 424 |
+
[[[[x * sx, y * sy] for x, y in kw["input_points"][0][0]]]],
|
| 425 |
+
dtype=torch.float32,
|
| 426 |
+
)
|
| 427 |
+
args["input_labels"] = torch.tensor(kw["input_labels"], dtype=torch.int64)
|
| 428 |
+
with torch.no_grad():
|
| 429 |
+
o = model(pixel_values=pixels, multimask_output=True, **args)
|
| 430 |
+
return o.pred_masks[0, 0].numpy(), o.iou_scores[0, 0].numpy()
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
CONTRACT = {
|
| 434 |
+
"sam2_encoder": (
|
| 435 |
+
[[1, 3, SIZE, SIZE]],
|
| 436 |
+
[[1, 256, SIZE // 16, SIZE // 16], [1, 256, SIZE // 4, SIZE // 4], [1, 256, SIZE // 8, SIZE // 8]],
|
| 437 |
+
),
|
| 438 |
+
"sam2_decoder": (
|
| 439 |
+
[[1, 256, SIZE // 16, SIZE // 16], [1, 256, SIZE // 16, SIZE // 16], [1, 1, "N", 256],
|
| 440 |
+
[1, 256, SIZE // 16, SIZE // 16], [1, 256, SIZE // 4, SIZE // 4], [1, 256, SIZE // 8, SIZE // 8]],
|
| 441 |
+
[[1, 3, SIZE // 4, SIZE // 4], [1, 1, 3], [1, 1, 1]],
|
| 442 |
+
),
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def program_io(pte_bytes):
|
| 447 |
+
"""Method names and forward() I/O (shape, dtype, dim order) as serialized in the program."""
|
| 448 |
+
from executorch.exir._serialize._program import deserialize_pte_binary
|
| 449 |
+
|
| 450 |
+
pte = deserialize_pte_binary(pte_bytes)
|
| 451 |
+
prog = pte.program if hasattr(pte, "program") else pte
|
| 452 |
+
|
| 453 |
+
def desc(plan, i):
|
| 454 |
+
v = plan.values[i].val
|
| 455 |
+
return {"shape": list(v.sizes), "dtype": v.scalar_type.name, "dim_order": list(v.dim_order)}
|
| 456 |
+
|
| 457 |
+
plan = {p.name: p for p in prog.execution_plan}
|
| 458 |
+
f = plan["forward"]
|
| 459 |
+
return {
|
| 460 |
+
"methods": sorted(plan),
|
| 461 |
+
"inputs": [desc(f, i) for i in f.inputs],
|
| 462 |
+
"outputs": [desc(f, i) for i in f.outputs],
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def check_contract(name, io, size, n_tokens):
|
| 467 |
+
"""The playground's programs: one `forward`, fp32, contiguous NCHW, these shapes."""
|
| 468 |
+
if size != SIZE:
|
| 469 |
+
return "skipped (non-default size)"
|
| 470 |
+
want_in, want_out = CONTRACT[name]
|
| 471 |
+
want_in = [[n_tokens if d == "N" else d for d in s] for s in want_in]
|
| 472 |
+
ok = (
|
| 473 |
+
io["methods"] == ["forward"]
|
| 474 |
+
and [t["shape"] for t in io["inputs"]] == want_in
|
| 475 |
+
and [t["shape"] for t in io["outputs"]] == want_out
|
| 476 |
+
and all(t["dtype"] == "FLOAT" and t["dim_order"] == list(range(len(t["shape"]))) for t in io["inputs"] + io["outputs"])
|
| 477 |
+
)
|
| 478 |
+
if not ok:
|
| 479 |
+
sys.exit(f"{name}: I/O does not match the previous contract: {json.dumps(io)}")
|
| 480 |
+
return "matches the previous export (shapes, fp32, contiguous dim order, single forward method)"
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def write_if_changed(path, data):
|
| 484 |
+
"""Leave an identical file alone; otherwise replace it with a new inode, so hard links made to an
|
| 485 |
+
earlier output elsewhere keep their content."""
|
| 486 |
+
data = bytes(data)
|
| 487 |
+
if path.exists():
|
| 488 |
+
if path.read_bytes() == data:
|
| 489 |
+
return
|
| 490 |
+
path.unlink()
|
| 491 |
+
path.write_bytes(data)
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def export_programs(enc, dec, enc_std, dec_std, pixels, dec_inputs):
|
| 495 |
+
with torch.no_grad():
|
| 496 |
+
ep_enc = torch.export.export(enc, (pixels,), strict=False)
|
| 497 |
+
ep_dec = torch.export.export(dec, dec_inputs, strict=False)
|
| 498 |
+
ep_enc_std = torch.export.export(enc_std, (pixels,), strict=False)
|
| 499 |
+
ep_dec_std = torch.export.export(dec_std, dec_inputs, strict=False)
|
| 500 |
+
return {
|
| 501 |
+
"sam2_encoder": (
|
| 502 |
+
ep_enc,
|
| 503 |
+
_common.lower_webgpu(ep_enc),
|
| 504 |
+
_common.lower_portable(ep_enc_std),
|
| 505 |
+
),
|
| 506 |
+
"sam2_decoder": (
|
| 507 |
+
ep_dec,
|
| 508 |
+
_common.lower_webgpu(ep_dec),
|
| 509 |
+
_common.lower_portable(ep_dec_std),
|
| 510 |
+
),
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def validation_images(args):
|
| 515 |
+
from huggingface_hub import hf_hub_download
|
| 516 |
+
|
| 517 |
+
out = []
|
| 518 |
+
p = Path(
|
| 519 |
+
hf_hub_download(
|
| 520 |
+
IMAGE["repo"],
|
| 521 |
+
IMAGE["file"],
|
| 522 |
+
repo_type=IMAGE["repo_type"],
|
| 523 |
+
revision=IMAGE["revision"],
|
| 524 |
+
)
|
| 525 |
+
)
|
| 526 |
+
digest = _common.sha256_file(p)
|
| 527 |
+
if digest != IMAGE["sha256"]:
|
| 528 |
+
sys.exit(f"{IMAGE['file']}: sha256 {digest} != {IMAGE['sha256']}")
|
| 529 |
+
out.append(
|
| 530 |
+
(
|
| 531 |
+
IMAGE["name"],
|
| 532 |
+
p,
|
| 533 |
+
digest,
|
| 534 |
+
IMAGE["prompts"],
|
| 535 |
+
f"hf://datasets/{IMAGE['repo']}@{IMAGE['revision']}/{IMAGE['file']}",
|
| 536 |
+
)
|
| 537 |
+
)
|
| 538 |
+
if args.assets_dir:
|
| 539 |
+
for name, spec in ASSET_IMAGES.items():
|
| 540 |
+
ap = Path(args.assets_dir) / name
|
| 541 |
+
d = _common.sha256_file(ap)
|
| 542 |
+
if spec["sha256"] and d != spec["sha256"]:
|
| 543 |
+
sys.exit(f"{ap}: sha256 {d} != {spec['sha256']}")
|
| 544 |
+
out.append(
|
| 545 |
+
(
|
| 546 |
+
name,
|
| 547 |
+
ap,
|
| 548 |
+
d,
|
| 549 |
+
spec["prompts"],
|
| 550 |
+
f"sample image {name}",
|
| 551 |
+
)
|
| 552 |
+
)
|
| 553 |
+
return out
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def validate(args, snap, model, tabs, progs, enc, dec, enc_std):
|
| 557 |
+
from transformers import Sam2Processor
|
| 558 |
+
|
| 559 |
+
val = {
|
| 560 |
+
"cpu_runtime": "executorch pybindings (portable_lib) from the executorch 1.5.1 wheel"
|
| 561 |
+
}
|
| 562 |
+
ep_enc, wg_enc, port_enc = progs["sam2_encoder"]
|
| 563 |
+
ep_dec, wg_dec, port_dec = progs["sam2_decoder"]
|
| 564 |
+
img_pe = torch.from_numpy(tabs["img_pos"])
|
| 565 |
+
dense = torch.from_numpy(tabs["dense"])
|
| 566 |
+
|
| 567 |
+
ref512 = load_model(snap, args.size)
|
| 568 |
+
ref1024 = load_model(snap, 1024)
|
| 569 |
+
processor = Sam2Processor.from_pretrained(snap)
|
| 570 |
+
other = Encoder(model, add_no_memory=not enc.add_no_memory).eval()
|
| 571 |
+
# Everything below runs the upstream do_pool; the export used the rewrite (checked to be exact).
|
| 572 |
+
upstream_pool()
|
| 573 |
+
|
| 574 |
+
for p_name, ep, wg in (("encoder", ep_enc, wg_enc), ("decoder", ep_dec, wg_dec)):
|
| 575 |
+
_, err = _common.try_run_webgpu_pte_on_cpu(wg.buffer, ep.example_inputs[0])
|
| 576 |
+
val[f"webgpu_{p_name}_pte_on_cpu"] = (
|
| 577 |
+
f"not runnable on CPU pybindings: {err}" if err else "ran"
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
val["images"] = {}
|
| 581 |
+
refs = Path(args.refs_dir) if args.refs_dir else None
|
| 582 |
+
refs_meta = {"prompt_tokens": args.prompt_tokens, "prompts": {}}
|
| 583 |
+
for name, path, digest, prompts, src in validation_images(args):
|
| 584 |
+
pixels, img = load_image(path, args.size)
|
| 585 |
+
W, H = img.size
|
| 586 |
+
with torch.no_grad():
|
| 587 |
+
ref = enc(pixels)
|
| 588 |
+
pool_windows_as_one_image()
|
| 589 |
+
rewritten = enc(pixels)
|
| 590 |
+
upstream_pool()
|
| 591 |
+
exported = ep_enc.module()(pixels)
|
| 592 |
+
std = enc_std(pixels)
|
| 593 |
+
emb_other = other(pixels)[0]
|
| 594 |
+
port = _common.run_pte(port_enc.buffer, (pixels,))
|
| 595 |
+
rec = {
|
| 596 |
+
"source": src,
|
| 597 |
+
"sha256": digest,
|
| 598 |
+
"size": [W, H],
|
| 599 |
+
"encoder": {},
|
| 600 |
+
"prompts": {},
|
| 601 |
+
}
|
| 602 |
+
for i, n in enumerate(["image_embeddings", "hr0", "hr1"]):
|
| 603 |
+
rec["encoder"][n] = {
|
| 604 |
+
"exported_vs_eager": _common.compare(
|
| 605 |
+
exported[i].numpy(), ref[i].numpy()
|
| 606 |
+
),
|
| 607 |
+
"pool_rewrite_vs_upstream_code_max_abs": float((rewritten[i] - ref[i]).abs().max()),
|
| 608 |
+
"eager_torch_sdpa_vs_eager_et_vk_sdpa": _common.compare(
|
| 609 |
+
std[i].numpy(), ref[i].numpy()
|
| 610 |
+
),
|
| 611 |
+
"portable_pte_vs_eager": _common.compare(port[i], ref[i].numpy()),
|
| 612 |
+
}
|
| 613 |
+
emb, hr0, hr1 = (t.contiguous() for t in ref)
|
| 614 |
+
if refs is not None and name == IMAGE["name"]:
|
| 615 |
+
refs.mkdir(parents=True, exist_ok=True)
|
| 616 |
+
refs.joinpath("pixels.f32").write_bytes(pixels.numpy().tobytes())
|
| 617 |
+
for i, t in enumerate((emb, hr0, hr1)):
|
| 618 |
+
refs.joinpath(f"enc_out{i}.f32").write_bytes(t.numpy().tobytes())
|
| 619 |
+
refs_meta.update(image=src, image_sha256=digest, size=[W, H])
|
| 620 |
+
for pname, toks in prompts.items():
|
| 621 |
+
dtoks = site_tokens(toks, args.prompt_tokens)
|
| 622 |
+
sparse = encode_tokens(dtoks, W, H, tabs, args.size)
|
| 623 |
+
ins = (emb, img_pe, sparse, dense, hr0, hr1)
|
| 624 |
+
with torch.no_grad():
|
| 625 |
+
eager = [t.numpy() for t in dec(*ins)]
|
| 626 |
+
exp = [t.numpy() for t in ep_dec.module()(*ins)]
|
| 627 |
+
alt_out = dec(emb_other, img_pe, sparse, dense, hr0, hr1)
|
| 628 |
+
alt, alt_iou = alt_out[0].numpy(), alt_out[1].numpy()
|
| 629 |
+
# End to end through both portable programs (encoder output -> decoder input).
|
| 630 |
+
pd = _common.run_pte(
|
| 631 |
+
port_dec.buffer,
|
| 632 |
+
(
|
| 633 |
+
torch.from_numpy(np.ascontiguousarray(port[0])),
|
| 634 |
+
img_pe,
|
| 635 |
+
sparse,
|
| 636 |
+
dense,
|
| 637 |
+
torch.from_numpy(np.ascontiguousarray(port[1])),
|
| 638 |
+
torch.from_numpy(np.ascontiguousarray(port[2])),
|
| 639 |
+
),
|
| 640 |
+
)
|
| 641 |
+
m_pte = pd[0][0]
|
| 642 |
+
u512_m, u512_iou = upstream_512_same_pixels(ref512, pixels, toks, W, H)
|
| 643 |
+
u_m, u_iou = upstream_reference(ref1024, processor, img, toks)
|
| 644 |
+
up = upsample(m_pte, W, H)
|
| 645 |
+
up_alt = upsample(alt[0], W, H)
|
| 646 |
+
best = int(np.argmax(u_iou))
|
| 647 |
+
own = int(np.argmax(np.asarray(pd[1]).reshape(-1)))
|
| 648 |
+
alt_own = int(np.argmax(alt_iou.reshape(-1)))
|
| 649 |
+
if refs is not None and name == IMAGE["name"]:
|
| 650 |
+
for i, t in enumerate(eager):
|
| 651 |
+
refs.joinpath(f"dec_{pname}_out{i}.f32").write_bytes(np.ascontiguousarray(t, np.float32).tobytes())
|
| 652 |
+
refs.joinpath(f"sparse_{pname}.f32").write_bytes(sparse.numpy().tobytes())
|
| 653 |
+
refs.joinpath(f"up512_{pname}_masks.f32").write_bytes(np.ascontiguousarray(u512_m, np.float32).tobytes())
|
| 654 |
+
refs_meta["prompts"][pname] = {"prompt": [list(t) for t in toks], "tokens": [list(t) for t in dtoks]}
|
| 655 |
+
rec["prompts"][pname] = {
|
| 656 |
+
"prompt": [list(t) for t in toks],
|
| 657 |
+
"decoder_tokens": [t[0] for t in dtoks],
|
| 658 |
+
"eager_vs_upstream_512_masks": _common.compare(eager[0][0], u512_m),
|
| 659 |
+
"eager_vs_upstream_512_iou_scores": _common.compare(eager[1], u512_iou),
|
| 660 |
+
"exported_vs_eager_masks": _common.compare(exp[0], eager[0]),
|
| 661 |
+
"portable_e2e_vs_eager_masks": _common.compare(pd[0], eager[0]),
|
| 662 |
+
"portable_e2e_vs_eager_iou_scores": _common.compare(pd[1], eager[1]),
|
| 663 |
+
"portable_e2e_vs_eager_object_score": _common.compare(pd[2], eager[2]),
|
| 664 |
+
"mask_iou_portable_vs_eager": [
|
| 665 |
+
iou(m_pte[i], eager[0][0][i]) for i in range(3)
|
| 666 |
+
],
|
| 667 |
+
"mask_iou_portable_vs_upstream_512_same_pixels": [
|
| 668 |
+
iou(m_pte[i], u512_m[i]) for i in range(3)
|
| 669 |
+
],
|
| 670 |
+
"mask_iou_portable_vs_upstream_1024_pipeline_at_image_res": [
|
| 671 |
+
iou(up[i], u_m[i]) for i in range(3)
|
| 672 |
+
],
|
| 673 |
+
"portable_iou_scores": np.asarray(pd[1]).reshape(-1).round(4).tolist(),
|
| 674 |
+
"upstream_512_iou_scores": u512_iou.round(4).tolist(),
|
| 675 |
+
"upstream_1024_iou_scores": u_iou.round(4).tolist(),
|
| 676 |
+
"upstream_1024_best_mask": best,
|
| 677 |
+
"best_mask_iou_portable_vs_upstream_1024": iou(up[best], u_m[best]),
|
| 678 |
+
"portable_object_score_logit": round(
|
| 679 |
+
float(np.asarray(pd[2]).reshape(-1)[0]), 4
|
| 680 |
+
),
|
| 681 |
+
"mask_area_fraction_portable": [
|
| 682 |
+
round(float((m_pte[i] > 0).mean()), 4) for i in range(3)
|
| 683 |
+
],
|
| 684 |
+
"upstream_1024_best_mask_vs_closest_export_mask": max(iou(up[i], u_m[best]) for i in range(3)),
|
| 685 |
+
"export_best_by_own_iou_head_vs_upstream_1024_best": iou(up[own], u_m[best]),
|
| 686 |
+
"variant_%s_no_memory_embedding" % ("without" if enc.add_no_memory else "with"): {
|
| 687 |
+
"mask_iou_vs_this_export": [iou(alt[0][i], eager[0][0][i]) for i in range(3)],
|
| 688 |
+
"mask_iou_vs_upstream_1024_pipeline_at_image_res": [iou(up_alt[i], u_m[i]) for i in range(3)],
|
| 689 |
+
"upstream_1024_best_mask_vs_closest_mask": max(iou(up_alt[i], u_m[best]) for i in range(3)),
|
| 690 |
+
"best_by_own_iou_head_vs_upstream_1024_best": iou(up_alt[alt_own], u_m[best]),
|
| 691 |
+
},
|
| 692 |
+
}
|
| 693 |
+
val["images"][name] = rec
|
| 694 |
+
print(json.dumps({name: rec}, indent=1), flush=True)
|
| 695 |
+
if refs is not None:
|
| 696 |
+
refs.joinpath("meta.json").write_text(json.dumps(refs_meta, indent=1) + "\n")
|
| 697 |
+
|
| 698 |
+
if args.internal_dir:
|
| 699 |
+
d = Path(args.internal_dir)
|
| 700 |
+
val["prompt_tables_vs_internal"] = {
|
| 701 |
+
f"prompt_{k}.bin": "bit-identical"
|
| 702 |
+
if (d / f"prompt_{k}.bin").read_bytes() == v.astype(np.float32).tobytes()
|
| 703 |
+
else "max abs %.3g"
|
| 704 |
+
% float(
|
| 705 |
+
np.abs(
|
| 706 |
+
np.fromfile(d / f"prompt_{k}.bin", dtype=np.float32) - v.reshape(-1)
|
| 707 |
+
).max()
|
| 708 |
+
)
|
| 709 |
+
for k, v in tabs.items()
|
| 710 |
+
}
|
| 711 |
+
return val
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
def main():
|
| 715 |
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
| 716 |
+
ap.add_argument("--out", required=True)
|
| 717 |
+
ap.add_argument("--weights", choices=sorted(WEIGHTS), default="sam2.1")
|
| 718 |
+
ap.add_argument(
|
| 719 |
+
"--size", type=int, default=SIZE, help="square input size (multiple of 32)"
|
| 720 |
+
)
|
| 721 |
+
ap.add_argument(
|
| 722 |
+
"--no-memory-embedding",
|
| 723 |
+
choices=["add", "omit"],
|
| 724 |
+
default="add",
|
| 725 |
+
help="add (upstream single-image behaviour, default) or omit (as the previous export)",
|
| 726 |
+
)
|
| 727 |
+
ap.add_argument(
|
| 728 |
+
"--assets-dir",
|
| 729 |
+
help="optional: directory of sample images for extra validation",
|
| 730 |
+
)
|
| 731 |
+
ap.add_argument(
|
| 732 |
+
"--internal-dir",
|
| 733 |
+
help="optional: compare prompt tables with an existing prompt_*.bin set",
|
| 734 |
+
)
|
| 735 |
+
ap.add_argument(
|
| 736 |
+
"--prompt-tokens",
|
| 737 |
+
type=int,
|
| 738 |
+
default=3,
|
| 739 |
+
help="sparse prompt tokens per decode (trailing extra pads are masked, so any prompt that fits matches upstream)",
|
| 740 |
+
)
|
| 741 |
+
ap.add_argument(
|
| 742 |
+
"--refs-dir",
|
| 743 |
+
help="optional: write CPU reference tensors for the browser harness (sam-car.png prompts)",
|
| 744 |
+
)
|
| 745 |
+
ap.add_argument(
|
| 746 |
+
"--skip-validation",
|
| 747 |
+
action="store_true",
|
| 748 |
+
help="export only (for the byte-identical re-run check)",
|
| 749 |
+
)
|
| 750 |
+
args = ap.parse_args()
|
| 751 |
+
out = Path(args.out)
|
| 752 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 753 |
+
torch.manual_seed(0)
|
| 754 |
+
|
| 755 |
+
snap, digest = snapshot(args.weights)
|
| 756 |
+
register_attention()
|
| 757 |
+
pool_windows_as_one_image()
|
| 758 |
+
add_nm = args.no_memory_embedding == "add"
|
| 759 |
+
model = freeze_hiera_pos_embed(
|
| 760 |
+
load_model(snap, args.size, attn="et_vk_sdpa"), args.size
|
| 761 |
+
)
|
| 762 |
+
enc, dec = Encoder(model, add_nm).eval(), Decoder(model).eval()
|
| 763 |
+
tabs = prompt_tables(model)
|
| 764 |
+
# Same weights with torch SDPA: only for the CPU portable .pte (et_vk.sdpa has no portable kernel).
|
| 765 |
+
model_std = freeze_hiera_pos_embed(
|
| 766 |
+
load_model(snap, args.size, attn="sdpa"), args.size
|
| 767 |
+
)
|
| 768 |
+
model_std_dec = load_model(snap, args.size, attn="torch_sdpa_padmask")
|
| 769 |
+
enc_std, dec_std = Encoder(model_std, add_nm).eval(), Decoder(model_std_dec).eval()
|
| 770 |
+
|
| 771 |
+
# Example inputs for export: a fixed-seed tensor (the graph does not depend on values).
|
| 772 |
+
gen = torch.Generator().manual_seed(0)
|
| 773 |
+
pixels = torch.randn(1, 3, args.size, args.size, generator=gen)
|
| 774 |
+
with torch.no_grad():
|
| 775 |
+
# Contiguous example inputs: torch.export records each input's dim order.
|
| 776 |
+
emb, hr0, hr1 = (t.contiguous() for t in enc(pixels))
|
| 777 |
+
sparse = encode_tokens(
|
| 778 |
+
site_tokens([("pos", args.size / 2, args.size / 2)], args.prompt_tokens),
|
| 779 |
+
args.size,
|
| 780 |
+
args.size,
|
| 781 |
+
tabs,
|
| 782 |
+
args.size,
|
| 783 |
+
)
|
| 784 |
+
dec_inputs = (
|
| 785 |
+
emb,
|
| 786 |
+
torch.from_numpy(tabs["img_pos"]),
|
| 787 |
+
sparse,
|
| 788 |
+
torch.from_numpy(tabs["dense"]),
|
| 789 |
+
hr0,
|
| 790 |
+
hr1,
|
| 791 |
+
)
|
| 792 |
+
progs = export_programs(enc, dec, enc_std, dec_std, pixels, dec_inputs)
|
| 793 |
+
|
| 794 |
+
files, graphs = {}, {}
|
| 795 |
+
for name, (_, wg, port) in progs.items():
|
| 796 |
+
write_if_changed(out / f"{name}.pte", wg.buffer)
|
| 797 |
+
write_if_changed(out / f"{name}_portable.pte", port.buffer)
|
| 798 |
+
graphs[name] = _common.delegate_summary(wg.buffer)
|
| 799 |
+
graphs[name]["io"] = program_io(wg.buffer)
|
| 800 |
+
graphs[name]["io_contract"] = check_contract(name, graphs[name]["io"], args.size, args.prompt_tokens)
|
| 801 |
+
if graphs[name]["non_delegated_kernels"] or len(graphs[name]["delegates"]) != 1:
|
| 802 |
+
sys.exit(f"{name}: expected one delegate and no CPU kernels: {json.dumps(graphs[name])[:2000]}")
|
| 803 |
+
for k, v in tabs.items():
|
| 804 |
+
write_if_changed(out / f"prompt_{k}.bin", v.astype(np.float32).tobytes())
|
| 805 |
+
for p in sorted(out.iterdir()):
|
| 806 |
+
if p.suffix in (".pte", ".bin"):
|
| 807 |
+
files[p.name] = {
|
| 808 |
+
"bytes": p.stat().st_size,
|
| 809 |
+
"sha256": _common.sha256_file(p),
|
| 810 |
+
}
|
| 811 |
+
if p.name.endswith("_portable.pte"):
|
| 812 |
+
files[p.name]["note"] = (
|
| 813 |
+
"CPU-only validation artifact (portable kernels, no delegate; torch SDPA instead of et_vk.sdpa); not for release"
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
w = WEIGHTS[args.weights]
|
| 817 |
+
q = args.size // 4
|
| 818 |
+
recipe = {
|
| 819 |
+
"model": f"{w['repo'].split('/')[1]} ({args.size} encoder/decoder split)",
|
| 820 |
+
"upstream": {
|
| 821 |
+
"model": w["repo"],
|
| 822 |
+
"revision": w["revision"],
|
| 823 |
+
"official": True,
|
| 824 |
+
"license": "Apache-2.0",
|
| 825 |
+
"files": {"model.safetensors": {"sha256": digest}},
|
| 826 |
+
"note": "transformers Sam2Model port published by the SAM 2 authors on the Hub",
|
| 827 |
+
},
|
| 828 |
+
"versions": _common.versions(),
|
| 829 |
+
"export": {
|
| 830 |
+
"api": "torch.export.export(strict=False) -> to_edge_transform_and_lower(partitioner=[WebGPUPartitioner()]) -> to_executorch(), one program per module",
|
| 831 |
+
"partitioner": "executorch.backends.webgpu.partitioner.WebGPUPartitioner() (default compile options; wraps VulkanPartitioner)",
|
| 832 |
+
"model_code": "transformers Sam2Model; attention through torch.ops.et_vk.sdpa (transformers attention backend, recipes/sam2-hiera-tiny.py:et_vk_attention); config image_size overridden 1024 -> %d"
|
| 833 |
+
% args.size,
|
| 834 |
+
"programs": {
|
| 835 |
+
"sam2_encoder.pte": {
|
| 836 |
+
"method": "forward",
|
| 837 |
+
"inputs": [
|
| 838 |
+
{
|
| 839 |
+
"name": "pixels",
|
| 840 |
+
"shape": [1, 3, args.size, args.size],
|
| 841 |
+
"dtype": "float32",
|
| 842 |
+
}
|
| 843 |
+
],
|
| 844 |
+
"outputs": [
|
| 845 |
+
{
|
| 846 |
+
"name": "image_embeddings",
|
| 847 |
+
"shape": list(emb.shape),
|
| 848 |
+
"dtype": "float32",
|
| 849 |
+
"note": "no_memory_embedding %s"
|
| 850 |
+
% ("added" if add_nm else "not added"),
|
| 851 |
+
},
|
| 852 |
+
{
|
| 853 |
+
"name": "hr0",
|
| 854 |
+
"shape": list(hr0.shape),
|
| 855 |
+
"dtype": "float32",
|
| 856 |
+
"note": "before mask_decoder.conv_s0",
|
| 857 |
+
},
|
| 858 |
+
{
|
| 859 |
+
"name": "hr1",
|
| 860 |
+
"shape": list(hr1.shape),
|
| 861 |
+
"dtype": "float32",
|
| 862 |
+
"note": "before mask_decoder.conv_s1",
|
| 863 |
+
},
|
| 864 |
+
],
|
| 865 |
+
},
|
| 866 |
+
"sam2_decoder.pte": {
|
| 867 |
+
"method": "forward",
|
| 868 |
+
"prompt_tokens": args.prompt_tokens,
|
| 869 |
+
"inputs": [
|
| 870 |
+
{"name": n, "shape": list(t.shape), "dtype": "float32"}
|
| 871 |
+
for n, t in zip(INPUT_NAMES, dec_inputs)
|
| 872 |
+
],
|
| 873 |
+
"outputs": [
|
| 874 |
+
{
|
| 875 |
+
"name": "masks",
|
| 876 |
+
"shape": [1, 3, q, q],
|
| 877 |
+
"dtype": "float32",
|
| 878 |
+
"note": "logits, mask tokens 1..3",
|
| 879 |
+
},
|
| 880 |
+
{"name": "iou_scores", "shape": [1, 1, 3], "dtype": "float32"},
|
| 881 |
+
{
|
| 882 |
+
"name": "object_score_logits",
|
| 883 |
+
"shape": [1, 1, 1],
|
| 884 |
+
"dtype": "float32",
|
| 885 |
+
},
|
| 886 |
+
],
|
| 887 |
+
},
|
| 888 |
+
},
|
| 889 |
+
"prompt_tables": {
|
| 890 |
+
f"prompt_{k}.bin": {
|
| 891 |
+
"shape": list(v.shape),
|
| 892 |
+
"dtype": "float32 little-endian",
|
| 893 |
+
}
|
| 894 |
+
for k, v in tabs.items()
|
| 895 |
+
},
|
| 896 |
+
"prompt_table_sources": {
|
| 897 |
+
"prompt_gaussian.bin": "shared_image_embedding.positional_embedding",
|
| 898 |
+
"prompt_point_embed_bg.bin": "prompt_encoder.point_embed.weight[0]",
|
| 899 |
+
"prompt_point_embed_fg.bin": "prompt_encoder.point_embed.weight[1]",
|
| 900 |
+
"prompt_box_tl.bin": "prompt_encoder.point_embed.weight[2]",
|
| 901 |
+
"prompt_box_br.bin": "prompt_encoder.point_embed.weight[3]",
|
| 902 |
+
"prompt_not_a_point.bin": "prompt_encoder.not_a_point_embed.weight[0]",
|
| 903 |
+
"prompt_img_pos.bin": "Sam2Model.get_image_wide_positional_embeddings() with image_size=%d"
|
| 904 |
+
% args.size,
|
| 905 |
+
"prompt_dense.bin": "prompt_encoder.no_mask_embed.weight broadcast to [1,256,%d,%d]"
|
| 906 |
+
% (args.size // 16, args.size // 16),
|
| 907 |
+
},
|
| 908 |
+
"graph_rewrites": [
|
| 909 |
+
"Hiera q-pooling of windowed tensors [nW,h,w,C] done as one batch-1 max_pool2d over the windows stacked along H (exact: h is a multiple of the stride); keeps all pools in the delegate",
|
| 910 |
+
"attention via torch.ops.et_vk.sdpa (same math as torch SDPA; checked in validation.json)",
|
| 911 |
+
"decoder: a not-a-point token directly after another one gets an additive -1e9 key bias in every attention (computed in-graph from the sparse input), so padding up to the fixed token count does not change the result",
|
| 912 |
+
"Hiera position embedding (bicubic pos_embed resize + tiled window embedding) for the fixed input size computed in eager and stored as a constant",
|
| 913 |
+
],
|
| 914 |
+
"differences_from_upstream_pipeline": [
|
| 915 |
+
"input %dx%d instead of 1024x1024 (masks %dx%d instead of 256x256)"
|
| 916 |
+
% (args.size, args.size, q, q),
|
| 917 |
+
"fixed %d prompt tokens per decode: points, box corners, one not-a-point pad, then padding pads that the decoder masks out (exactly upstream's prompt for any prompt that fits; larger prompts are composed by the site)" % args.prompt_tokens,
|
| 918 |
+
"no mask prompt input (dense = no_mask_embed)",
|
| 919 |
+
],
|
| 920 |
+
"preprocessing_outside_graph": "RGB, resize to %dx%d without keeping aspect, /255, ImageNet mean/std"
|
| 921 |
+
% (args.size, args.size),
|
| 922 |
+
"dtype": "float32",
|
| 923 |
+
"quantization": "none (fp32)",
|
| 924 |
+
},
|
| 925 |
+
"files": files,
|
| 926 |
+
"graph": graphs,
|
| 927 |
+
"command": _common.command_line(out),
|
| 928 |
+
}
|
| 929 |
+
(out / "recipe.json").write_text(json.dumps(recipe, indent=2) + "\n")
|
| 930 |
+
print(json.dumps({"files": files, "graph": graphs}, indent=1), flush=True)
|
| 931 |
+
if args.skip_validation:
|
| 932 |
+
return
|
| 933 |
+
val = validate(args, snap, model, tabs, progs, enc, dec, enc_std)
|
| 934 |
+
(out / "validation.json").write_text(json.dumps(val, indent=2) + "\n")
|
| 935 |
+
print(json.dumps(val, indent=1))
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
if __name__ == "__main__":
|
| 939 |
+
main()
|
manifest.json
CHANGED
|
@@ -44,13 +44,13 @@
|
|
| 44 |
},
|
| 45 |
{
|
| 46 |
"path": "sam2_decoder.pte",
|
| 47 |
-
"bytes":
|
| 48 |
-
"sha256": "
|
| 49 |
},
|
| 50 |
{
|
| 51 |
"path": "sam2_encoder.pte",
|
| 52 |
-
"bytes":
|
| 53 |
-
"sha256": "
|
| 54 |
}
|
| 55 |
]
|
| 56 |
}
|
|
|
|
| 44 |
},
|
| 45 |
{
|
| 46 |
"path": "sam2_decoder.pte",
|
| 47 |
+
"bytes": 16949252,
|
| 48 |
+
"sha256": "57b91034acc71be2ce1835ea546a174295daee1046e40db0743e0238ef11e278"
|
| 49 |
},
|
| 50 |
{
|
| 51 |
"path": "sam2_encoder.pte",
|
| 52 |
+
"bytes": 115248512,
|
| 53 |
+
"sha256": "30f9fb469b779558b6b5c7c3eb995c50e1213ffbe082e31430c929ba2b3ffe20"
|
| 54 |
}
|
| 55 |
]
|
| 56 |
}
|
sam2_decoder.pte
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:57b91034acc71be2ce1835ea546a174295daee1046e40db0743e0238ef11e278
|
| 3 |
+
size 16949252
|
sam2_encoder.pte
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:30f9fb469b779558b6b5c7c3eb995c50e1213ffbe082e31430c929ba2b3ffe20
|
| 3 |
+
size 115248512
|