Merge tracker + backbone, drop kino references
Browse files- README.md +101 -0
- backbone/models/sam3_vision_backbone.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- backbone/models/sam3_vision_backbone.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- backbone/models/sam3_vision_backbone.mlpackage/Manifest.json +18 -0
- tracker/models/dense_sam3_trackstep.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- tracker/models/dense_sam3_trackstep.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- tracker/models/dense_sam3_trackstep.mlpackage/Manifest.json +18 -0
- tracker/models/dense_sam3_trackstep.mlpackage/executorch_debug_handle_mapping.json +0 -0
- tracker/models/dense_sam3_trackstep_fp32.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- tracker/models/dense_sam3_trackstep_fp32.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- tracker/models/dense_sam3_trackstep_fp32.mlpackage/Manifest.json +18 -0
- tracker/models/dense_sam3_trackstep_fp32.mlpackage/executorch_debug_handle_mapping.json +0 -0
- tracker/patches/optimize_state.patch +22 -0
- tracker/scripts/ane_compile_test.py +61 -0
- tracker/scripts/common.py +153 -0
- tracker/scripts/convert_fp16.py +52 -0
- tracker/scripts/coreml_traj_test.py +52 -0
- tracker/scripts/dense_wrapper.py +329 -0
- tracker/scripts/eager_baseline.py +112 -0
- tracker/scripts/export_coreml.py +102 -0
- tracker/scripts/export_fp32.py +91 -0
- tracker/scripts/test_cross_state.py +67 -0
- tracker/scripts/toy_ane_test.py +17 -0
- tracker/scripts/toy_bisect.py +37 -0
- tracker/scripts/toy_minstate.py +23 -0
- tracker/scripts/verify_coreml_lean.py +70 -0
- tracker/triton_stub/triton/__init__.py +12 -0
- tracker/triton_stub/triton/language.py +12 -0
README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: sam-license
|
| 4 |
+
license_link: https://github.com/facebookresearch/sam3/blob/main/LICENSE
|
| 5 |
+
library_name: coreml
|
| 6 |
+
pipeline_tag: mask-generation
|
| 7 |
+
base_model: facebook/sam3
|
| 8 |
+
tags:
|
| 9 |
+
- coreml
|
| 10 |
+
- sam3
|
| 11 |
+
- sam3.1
|
| 12 |
+
- segmentation
|
| 13 |
+
- video-segmentation
|
| 14 |
+
- object-tracking
|
| 15 |
+
- object-multiplex
|
| 16 |
+
- vision-backbone
|
| 17 |
+
- stateful
|
| 18 |
+
- fp16
|
| 19 |
+
- apple-silicon
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
# SAM3.1 → CoreML (Apple Silicon)
|
| 23 |
+
|
| 24 |
+
Real SAM3.1 weights exported to CoreML for on-device video segmentation on Apple Silicon: a stateless per-frame vision backbone plus a stateful multiplex tracker (`track_step`), designed to run together.
|
| 25 |
+
|
| 26 |
+
- [`backbone/`](backbone) — per-frame image encoder (stateless)
|
| 27 |
+
- [`tracker/`](tracker) — per-frame `track_step` as a stateful CoreML package (multiplex_count=16)
|
| 28 |
+
|
| 29 |
+
## Contract
|
| 30 |
+
|
| 31 |
+
The backbone feeds the tracker:
|
| 32 |
+
|
| 33 |
+
```
|
| 34 |
+
image [1,3,1008,1008] fp32 (normalized to [-1,1])
|
| 35 |
+
-> vis72 [5184,1,256]
|
| 36 |
+
-> hires0 [1,32,288,288] # post sam_mask_decoder.conv_s0
|
| 37 |
+
-> hires1 [1,64,144,144] # post conv_s1
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
Trunk + propagation TriViTDetNeck + `conv_s0`/`conv_s1`. `use_rope_real=True`. Exported via `torch.jit.trace` → coremltools 9 / iOS18. Deploy `CPU_AND_GPU` (~2.4s/frame isolated on Apple Silicon).
|
| 41 |
+
|
| 42 |
+
## Tracker: `tracker/`
|
| 43 |
+
|
| 44 |
+
SAM3.1's per-frame `track_step` as a stateful CoreML package on Apple Silicon. Real weights, `multiplex_count=16`, runs on GPU (`CPU_AND_GPU`). Not a product — export + parity proof.
|
| 45 |
+
|
| 46 |
+
### Layout
|
| 47 |
+
|
| 48 |
+
- `models/dense_sam3_trackstep.mlpackage` (fp16, 86M) — deploy this. `compute_units=CPU_AND_GPU`.
|
| 49 |
+
- `models/dense_sam3_trackstep_fp32.mlpackage` (fp32, 170M) — precision reference.
|
| 50 |
+
- `scripts/dense_wrapper.py` — rewrites dict memory banks + `MultiplexState` dynamic buckets into fixed `register_buffer`s + validity masks (shift+cat). Warmup = additive `-80` key mask from `mem_valid`/`ptr_valid`. Calls `transformer.encoder`, `_forward_sam_heads`, `_encode_new_memory` as-is once state is frozen.
|
| 51 |
+
- `scripts/common.py` — CPU workarounds, checkpoint download + prefix strip, mux=16.
|
| 52 |
+
- `scripts/eager_baseline.py` — builds `eager_cache.pt` (~388M, not shipped; regenerate once).
|
| 53 |
+
- `scripts/convert_fp16.py`, `export_fp32.py`, `export_coreml.py` — convert. `verify_coreml_lean.py` — low-RAM parity from a saved package.
|
| 54 |
+
- `scripts/toy_bisect.py`, `test_cross_state.py`, `ane_compile_test.py`, `toy_*` — repros for the bugs below.
|
| 55 |
+
- `patches/optimize_state.patch` — required for coremltools 9.0.
|
| 56 |
+
- `triton_stub/` — so `sam3/model/edt.py`'s bare `import triton` doesn't die on Mac (no wheel). The EDT kernel itself is CUDA; track_step export doesn't need it. Swap the call site for `cv2.distanceTransform` / scipy if you want a real CPU path.
|
| 57 |
+
|
| 58 |
+
### Numbers
|
| 59 |
+
|
| 60 |
+
Real SAM3.1 tracker weights, 16GB Apple Silicon, 1008px masks, 72×72 memory grid, mux=16.
|
| 61 |
+
|
| 62 |
+
Dense wrapper vs eager `track_step` (max-abs; values up to ~1e3 → rel ~1e-7–1e-5):
|
| 63 |
+
|
| 64 |
+
| frame | pred_masks | pred_masks_high_res | object_score_logits | ious | new maskmem | new obj_ptr |
|
| 65 |
+
|---|---|---|---|---|---|---|
|
| 66 |
+
| 16 (steady) | 5.96e-05 | 5.46e-05 | 2.16e-05 | 5.25e-04 | 3.61e-05 | 8.64e-06 |
|
| 67 |
+
| 3 (warmup) | 9.35e-05 | 8.39e-05 | 1.90e-06 | 7.06e-05 | — | — |
|
| 68 |
+
|
| 69 |
+
CoreML fp16 vs eager (`CPU_AND_GPU`, frames 1→16 from zero state, `verify_coreml_lean.py`): frame 3 `low_rel=1.7e-5 osl_rel=3.1e-3`, frame 16 `low_rel=7.5e-6 osl_rel=3.1e-3`, mask sign-agree 1.0, zero `appear_flips`, worst rel 3.1e-3. Earlier random-weight fp16 appear-flips don't show up with real logits.
|
| 70 |
+
|
| 71 |
+
`torch.export` + `coremltools.convert(..., minimum_deployment_target=iOS18)` both fine for fp16/fp32 (fp16 convert ~13s).
|
| 72 |
+
|
| 73 |
+
### Bugs / limits
|
| 74 |
+
|
| 75 |
+
**coremltools 9.0** — `optimize_state.py::canonicalize_inplace_pattern` snapshots the op list, then deletes ops while iterating; later states grab a dead `before_op` → `KeyError` in `op_to_node`. Hits anything with ≥3 rolling state buffers via shift+cat (this tracker's mem/img/ptr banks). Torch 2.7.0 pin was a red herring. Patch walks past removed ops. Repro: `toy_bisect.py`. File upstream if you care.
|
| 76 |
+
|
| 77 |
+
**fp32 predict OOMs on 16GB** — convert is fine; `CPU_ONLY` predict at mux=16 blows past 16GB (masked SDPA over ~36k-token mem × 16 objs × 1008px, fp32 intermediates). fp16 + GPU fits. Need more RAM for fp32 runtime.
|
| 78 |
+
|
| 79 |
+
**ANE** — every stateful CoreML model fails ANE compile (`ANECCompile` error -14) on macOS 26.5.2 / coremltools 9.0, including a one-buffer + Linear toy. Stateless attention ANE-compiles fine. Platform wall, not this graph. Deploy on `CPU_AND_GPU`: ~0.7s/frame steady, ~7s first call (JIT). Repros in `toy_*` / `ane_compile_test.py`.
|
| 80 |
+
|
| 81 |
+
### Not wired yet
|
| 82 |
+
|
| 83 |
+
- One conditioning frame only. Model allows `max_cond_frames_in_attn=4`.
|
| 84 |
+
|
| 85 |
+
## Backbone: `backbone/`
|
| 86 |
+
|
| 87 |
+
Per-frame image encoder, stateless. Feeds the tracker above with `vis72` / `hires0` / `hires1` (see contract above).
|
| 88 |
+
|
| 89 |
+
### Layout
|
| 90 |
+
|
| 91 |
+
- `models/sam3_vision_backbone.mlpackage` (~875MB fp16)
|
| 92 |
+
|
| 93 |
+
## Weights & license
|
| 94 |
+
|
| 95 |
+
`tracker/models/*.mlpackage` and `backbone/models/*.mlpackage` ship **real SAM3.1 weights** (tracker: 457 tensors from `sam3.1_multiplex.pt`, `tracker.model.` prefix stripped; backbone: `detector.backbone.vision_backbone.*`). Meta gates the original on `facebook/sam3`; export scripts pull from the community mirror `AEmotionStudio/sam3.1` (`tracker/scripts/common.py::resolve_checkpoint`, override with `SAM3_HF_REPO` / `SAM3_CHECKPOINT`). That mirror is unofficial — check against Meta's release before you trust it.
|
| 96 |
+
|
| 97 |
+
Derivative of Meta's SAM Materials → subject to Meta's [SAM License](https://github.com/facebookresearch/sam3/blob/main/LICENSE) (share-alike, field-of-use, attribution). Not a permissive license. Redistributing the weight binaries means honoring that.
|
| 98 |
+
|
| 99 |
+
## Env
|
| 100 |
+
|
| 101 |
+
Python 3.9. Torch **2.7.0** + torchvision **0.22.0** (coremltools 9.0 ceiling; `timm` et al. will silently bump torch — check). `coremltools==9.0` with `patches/optimize_state.patch` on `…/optimize_state.py`. Register a `where.scalarother` op at convert (upstream only ships `scalarself`). State inputs `.contiguous()`. `use_rope_real=True` (no complex tensors in CoreML). Drop the triton stub from `sys.path`/`sys.modules` before `torch.export` (`common.hide_triton_stub`).
|
backbone/models/sam3_vision_backbone.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c4474e3bf070f097c2a47e5814c33d9fe77bb416bac30011a0e20dd334dfe17
|
| 3 |
+
size 538552
|
backbone/models/sam3_vision_backbone.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:94838c0023ea7308bc86782b2f2c124470a3c2d7739961ef2c956c1932a5e178
|
| 3 |
+
size 916622976
|
backbone/models/sam3_vision_backbone.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"08F5CD35-FEAC-45F5-9D32-511890C3F562": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"E4A741A9-FAEE-42AF-893D-1BBFBFD4F9A6": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "E4A741A9-FAEE-42AF-893D-1BBFBFD4F9A6"
|
| 18 |
+
}
|
tracker/models/dense_sam3_trackstep.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6fcc12ed3815d28f9a271774f71b2e05a02c201f633bbba9cc157ba468930ef
|
| 3 |
+
size 314910
|
tracker/models/dense_sam3_trackstep.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:742aba93b515b8ccdc6935c557a8767115538a9fd6009164786f77514ccef6e4
|
| 3 |
+
size 89035968
|
tracker/models/dense_sam3_trackstep.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"5C239A58-0E5C-41C9-8A56-D0FB690285BA": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"DFD8B1C7-D690-4ECD-911C-987F37641012": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "DFD8B1C7-D690-4ECD-911C-987F37641012"
|
| 18 |
+
}
|
tracker/models/dense_sam3_trackstep.mlpackage/executorch_debug_handle_mapping.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tracker/models/dense_sam3_trackstep_fp32.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee221f2a9b0a2ac1e5798a0462a9e0d038d9e6f3a8d9580363e6f27d7b06f39d
|
| 3 |
+
size 284296
|
tracker/models/dense_sam3_trackstep_fp32.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2f57bab04b7ef412f7c3061bfdb1a0a5cdb01165bf61f4fb562e0aa366a1dac5
|
| 3 |
+
size 178052032
|
tracker/models/dense_sam3_trackstep_fp32.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"86FEDE2A-0538-44EF-9A45-F1DB30FED223": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"D1A362A2-9B47-4E58-BB64-CD42D4A1D425": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "D1A362A2-9B47-4E58-BB64-CD42D4A1D425"
|
| 18 |
+
}
|
tracker/models/dense_sam3_trackstep_fp32.mlpackage/executorch_debug_handle_mapping.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tracker/patches/optimize_state.patch
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--- /var/folders/n8/bv7zgpwx3b9crc3mbp6zqb5w0000gn/T/tmp.eWO02SiwSF/coremltools/converters/mil/mil/passes/defs/optimize_state.py 2025-11-03 23:10:10
|
| 2 |
+
+++ /private/tmp/claude-501/-Users-aiden-Developer-Kino-kino/54f28767-a77e-430f-881b-85e18da4ab2e/scratchpad/.venv/lib/python3.9/site-packages/coremltools/converters/mil/mil/passes/defs/optimize_state.py 2026-07-23 21:53:13
|
| 3 |
+
@@ -87,7 +87,18 @@
|
| 4 |
+
|
| 5 |
+
coreml_update_state_ops = self._try_find_child_coreml_update_state_ops(op)
|
| 6 |
+
for coreml_update_state_op in coreml_update_state_ops:
|
| 7 |
+
- before_op = block_operation_list[block_operation_list.index(op) + 1]
|
| 8 |
+
+ # PATCued: block_operation_list is a stale snapshot; earlier iterations
|
| 9 |
+
+ # may have removed ops (their enclosing_block is None). Walk forward to
|
| 10 |
+
+ # the next op still present in the block, else insert at block end.
|
| 11 |
+
+ idx = block_operation_list.index(op) + 1
|
| 12 |
+
+ while (
|
| 13 |
+
+ idx < len(block_operation_list)
|
| 14 |
+
+ and block_operation_list[idx].enclosing_block is None
|
| 15 |
+
+ ):
|
| 16 |
+
+ idx += 1
|
| 17 |
+
+ before_op = (
|
| 18 |
+
+ block_operation_list[idx] if idx < len(block_operation_list) else None
|
| 19 |
+
+ )
|
| 20 |
+
scopes = self._construct_scope_info_list_from_op_scopes(op)
|
| 21 |
+
with mb.scope(*scopes):
|
| 22 |
+
immediate_coreml_update_state = mb.coreml_update_state(
|
tracker/scripts/ane_compile_test.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Step 4b: attempt on-device ANE compile of the converted model.
|
| 2 |
+
Loads dense_sam3_trackstep.mlpackage with CPU_AND_NE and runs one predict.
|
| 3 |
+
The ANECompiler runs at load; failures surface as load errors or CPU fallback.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import time
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
import common
|
| 12 |
+
import dense_wrapper as dw
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
import coremltools as ct
|
| 17 |
+
|
| 18 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 19 |
+
# need model only for input synthesis
|
| 20 |
+
model = common.build_model()
|
| 21 |
+
inputs = dw.frame_inputs(model, cache, cache["target_frame"])
|
| 22 |
+
|
| 23 |
+
unit = getattr(ct.ComputeUnit, sys.argv[1] if len(sys.argv) > 1 else "CPU_AND_NE")
|
| 24 |
+
print(f"loading with compute_units={unit} ...")
|
| 25 |
+
t0 = time.time()
|
| 26 |
+
mlmodel = ct.models.MLModel("dense_sam3_trackstep.mlpackage",
|
| 27 |
+
compute_units=unit)
|
| 28 |
+
print(f"load OK in {time.time()-t0:.1f}s")
|
| 29 |
+
|
| 30 |
+
in_names = [i.name for i in mlmodel.input_description._fd_spec]
|
| 31 |
+
feed = {n: v.numpy().astype(np.float32) for n, v in zip(in_names, inputs)}
|
| 32 |
+
state = mlmodel.make_state()
|
| 33 |
+
t0 = time.time()
|
| 34 |
+
out = mlmodel.predict(feed, state=state)
|
| 35 |
+
print(f"predict 1 OK in {time.time()-t0:.1f}s")
|
| 36 |
+
t0 = time.time()
|
| 37 |
+
out = mlmodel.predict(feed, state=state)
|
| 38 |
+
print(f"predict 2 OK in {time.time()-t0:.1f}s")
|
| 39 |
+
print("outputs:", {k: np.asarray(v).shape for k, v in out.items()})
|
| 40 |
+
|
| 41 |
+
# which units actually got used?
|
| 42 |
+
try:
|
| 43 |
+
plan = ct.models.compute_plan.MLComputePlan.load_from_path(
|
| 44 |
+
mlmodel.get_compiled_model_path(), compute_units=unit
|
| 45 |
+
)
|
| 46 |
+
counts = {}
|
| 47 |
+
prog = plan.model_structure.program
|
| 48 |
+
fn = prog.functions["main"]
|
| 49 |
+
for op in fn.block.operations:
|
| 50 |
+
usage = plan.get_compute_device_usage_for_mlprogram_operation(op)
|
| 51 |
+
if usage is None:
|
| 52 |
+
continue
|
| 53 |
+
dev = type(usage.preferred_compute_device).__name__
|
| 54 |
+
counts[dev] = counts.get(dev, 0) + 1
|
| 55 |
+
print("op placement:", counts)
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"compute plan inspection failed: {type(e).__name__}: {e}")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
main()
|
tracker/scripts/common.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared setup: CPU workarounds + model build for SAM3 multiplex video model."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
SP = os.path.dirname(os.path.abspath(__file__))
|
| 7 |
+
sys.path.insert(0, os.path.join(SP, "triton_stub")) # satisfy `import triton`
|
| 8 |
+
sys.path.insert(0, os.path.join(SP, "sam3"))
|
| 9 |
+
|
| 10 |
+
import torch # noqa: E402
|
| 11 |
+
|
| 12 |
+
# CPU-run workarounds: sam3 hardcodes .cuda()/.pin_memory() in eval paths
|
| 13 |
+
torch.Tensor.cuda = lambda self, *a, **kw: self
|
| 14 |
+
torch.Tensor.pin_memory = lambda self, *a, **kw: self
|
| 15 |
+
|
| 16 |
+
MULTIPLEX_COUNT = 16 # matches released SAM3.1 multiplex checkpoint
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Official facebook/sam3.1 is gated; open mirror hosts the same multiplex weights.
|
| 20 |
+
SAM31_HF_REPO = os.environ.get("SAM3_HF_REPO", "AEmotionStudio/sam3.1")
|
| 21 |
+
SAM31_CKPT_NAME = "sam3.1_multiplex.pt"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def resolve_checkpoint(checkpoint_path=None):
|
| 25 |
+
"""Return local path to sam3.1_multiplex.pt (download via HF if needed)."""
|
| 26 |
+
if checkpoint_path:
|
| 27 |
+
return checkpoint_path
|
| 28 |
+
env = os.environ.get("SAM3_CHECKPOINT")
|
| 29 |
+
if env:
|
| 30 |
+
return env
|
| 31 |
+
from huggingface_hub import hf_hub_download
|
| 32 |
+
|
| 33 |
+
return hf_hub_download(repo_id=SAM31_HF_REPO, filename=SAM31_CKPT_NAME)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def hide_triton_stub():
|
| 37 |
+
"""Remove the triton stub after all sam3 modules are imported, so torch
|
| 38 |
+
inductor's `import triton` fails cleanly into its `triton = None` path."""
|
| 39 |
+
stub = os.path.join(SP, "triton_stub")
|
| 40 |
+
while stub in sys.path:
|
| 41 |
+
sys.path.remove(stub)
|
| 42 |
+
for k in list(sys.modules):
|
| 43 |
+
if k == "triton" or k.startswith("triton."):
|
| 44 |
+
del sys.modules[k]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _tracker_state_dict(ckpt):
|
| 48 |
+
"""Extract tracker weights from merged SAM3.1 multiplex checkpoint.
|
| 49 |
+
|
| 50 |
+
HF / mirror file stores full predictor keys (`tracker.model.*` +
|
| 51 |
+
`detector.*`). Tracker-only module wants the stripped `tracker.model.`
|
| 52 |
+
prefix. Also accepts a bare tracker state_dict or `{"model": ...}`.
|
| 53 |
+
"""
|
| 54 |
+
if isinstance(ckpt, dict) and "model" in ckpt and isinstance(ckpt["model"], dict):
|
| 55 |
+
ckpt = ckpt["model"]
|
| 56 |
+
if any(k.startswith("tracker.model.") for k in ckpt):
|
| 57 |
+
return {
|
| 58 |
+
k[len("tracker.model.") :]: v
|
| 59 |
+
for k, v in ckpt.items()
|
| 60 |
+
if k.startswith("tracker.model.")
|
| 61 |
+
}
|
| 62 |
+
return ckpt
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def build_model(
|
| 66 |
+
multiplex_count=MULTIPLEX_COUNT,
|
| 67 |
+
checkpoint_path=None,
|
| 68 |
+
load_checkpoint=True,
|
| 69 |
+
seed=1234,
|
| 70 |
+
):
|
| 71 |
+
"""Build multiplex video tracker. Default: real SAM3.1 weights, mux=16.
|
| 72 |
+
|
| 73 |
+
Set load_checkpoint=False for random-weight debug (uses seed).
|
| 74 |
+
"""
|
| 75 |
+
from sam3.model_builder import build_sam3_multiplex_video_model
|
| 76 |
+
|
| 77 |
+
if not load_checkpoint:
|
| 78 |
+
torch.manual_seed(seed)
|
| 79 |
+
|
| 80 |
+
# Always construct without builder-side load: merged HF ckpt needs key strip.
|
| 81 |
+
model = build_sam3_multiplex_video_model(
|
| 82 |
+
checkpoint_path=None,
|
| 83 |
+
load_from_HF=False,
|
| 84 |
+
multiplex_count=multiplex_count,
|
| 85 |
+
use_fa3=False,
|
| 86 |
+
use_rope_real=True, # avoid complex tensors (CoreML cannot represent them)
|
| 87 |
+
device="cpu",
|
| 88 |
+
strict_state_dict_loading=False,
|
| 89 |
+
)
|
| 90 |
+
# Drop image backbone before load: track_step uses precomputed feats; HF
|
| 91 |
+
# multiplex file also keeps backbone under detector.*, not tracker.model.*.
|
| 92 |
+
model.backbone = None
|
| 93 |
+
|
| 94 |
+
if load_checkpoint:
|
| 95 |
+
ckpt_path = resolve_checkpoint(checkpoint_path)
|
| 96 |
+
print(f"loading checkpoint: {ckpt_path}")
|
| 97 |
+
raw = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
| 98 |
+
sd = _tracker_state_dict(raw)
|
| 99 |
+
missing, unexpected = model.load_state_dict(sd, strict=True)
|
| 100 |
+
assert not missing and not unexpected, (missing, unexpected)
|
| 101 |
+
print(f"loaded tracker weights: {len(sd)} tensors")
|
| 102 |
+
|
| 103 |
+
model.eval()
|
| 104 |
+
model.requires_grad_(False)
|
| 105 |
+
return model
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def make_multiplex_state(model, num_objects=MULTIPLEX_COUNT):
|
| 109 |
+
return model.multiplex_controller.get_state(
|
| 110 |
+
num_objects, torch.device("cpu"), torch.float32, random=False
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def synth_frame_features(model, frame_idx, seed=777):
|
| 115 |
+
"""Deterministic synthetic backbone features for one frame (stand-in for the
|
| 116 |
+
stateless image encoder, which converts separately)."""
|
| 117 |
+
g = torch.Generator().manual_seed(seed * 100003 + frame_idx)
|
| 118 |
+
e = model.sam_image_embedding_size # 72
|
| 119 |
+
sizes = [(4 * e, 4 * e), (2 * e, 2 * e), (e, e)]
|
| 120 |
+
# levels 0/1 are pre-projected by sam_mask_decoder.conv_s0/s1 upstream of
|
| 121 |
+
# track_step (see forward_image), so channels are 32/64, not 256
|
| 122 |
+
chans = [32, 64, 256]
|
| 123 |
+
feats = [torch.randn((h * w, 1, c), generator=g) * 0.5
|
| 124 |
+
for (h, w), c in zip(sizes, chans)]
|
| 125 |
+
return feats, sizes
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def pos_embed_72(model):
|
| 129 |
+
"""Constant sine positional embedding of the 72x72 grid, (HW, 1, C)."""
|
| 130 |
+
from sam3.model.position_encoding import PositionEmbeddingSine
|
| 131 |
+
|
| 132 |
+
pe = PositionEmbeddingSine(
|
| 133 |
+
num_pos_feats=256, normalize=True, scale=None, temperature=10000
|
| 134 |
+
)
|
| 135 |
+
e = model.sam_image_embedding_size
|
| 136 |
+
with torch.no_grad():
|
| 137 |
+
pos = pe(torch.zeros(1, 1, e, e)) # (1,256,e,e)
|
| 138 |
+
return pos.flatten(2).permute(2, 0, 1) # (HW,1,256)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def init_masks(num_objects=MULTIPLEX_COUNT, size=1008):
|
| 142 |
+
"""Non-overlapping square blobs for up to 16 objects: (O,1,size,size)."""
|
| 143 |
+
m = torch.zeros(num_objects, 1, size, size)
|
| 144 |
+
# 4x4 grid of blobs inside 1008
|
| 145 |
+
cols = 4
|
| 146 |
+
cell = size // cols
|
| 147 |
+
margin = cell // 8
|
| 148 |
+
for i in range(num_objects):
|
| 149 |
+
r, c = divmod(i, cols)
|
| 150 |
+
r0, c0 = r * cell + margin, c * cell + margin
|
| 151 |
+
r1, c1 = (r + 1) * cell - margin, (c + 1) * cell - margin
|
| 152 |
+
m[i, 0, r0:r1, c0:c1] = 1.0
|
| 153 |
+
return m
|
tracker/scripts/convert_fp16.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convert-only fp16 export of DenseTrackStep (real SAM3.1 weights) and save.
|
| 2 |
+
Splits convert from verify so the RAM-heavy predict loop doesn't OOM the
|
| 3 |
+
convert on a 16GB host. Verify separately with verify_coreml_lean.py.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
import common
|
| 10 |
+
import dense_wrapper as dw
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 15 |
+
wrapper, model = dw.build_wrapper()
|
| 16 |
+
inputs = dw.frame_inputs(model, cache, 3) # mid frame exercises cond-tpos path
|
| 17 |
+
|
| 18 |
+
common.hide_triton_stub()
|
| 19 |
+
with torch.no_grad():
|
| 20 |
+
try:
|
| 21 |
+
ep = torch.export.export(wrapper, inputs)
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"strict export failed ({type(e).__name__}); retry strict=False")
|
| 24 |
+
ep = torch.export.export(wrapper, inputs, strict=False)
|
| 25 |
+
ep = ep.run_decompositions({})
|
| 26 |
+
print("torch.export OK")
|
| 27 |
+
|
| 28 |
+
import coremltools as ct
|
| 29 |
+
from coremltools.converters.mil.frontend.torch.torch_op_registry import (
|
| 30 |
+
register_torch_op,
|
| 31 |
+
)
|
| 32 |
+
from coremltools.converters.mil.frontend.torch.ops import _get_inputs
|
| 33 |
+
from coremltools.converters.mil.mil import Builder as mb
|
| 34 |
+
|
| 35 |
+
@register_torch_op(torch_alias=["where.scalarother"])
|
| 36 |
+
def where_scalarother(context, node):
|
| 37 |
+
cond, a, b = _get_inputs(context=context, node=node, expected=3)
|
| 38 |
+
context.add(mb.select(cond=cond, a=a, b=b), node.name)
|
| 39 |
+
|
| 40 |
+
t0 = time.time()
|
| 41 |
+
mlmodel = ct.convert(
|
| 42 |
+
ep,
|
| 43 |
+
minimum_deployment_target=ct.target.iOS18,
|
| 44 |
+
compute_units=ct.ComputeUnit.CPU_ONLY, # convert/load only; deploy sets CU
|
| 45 |
+
)
|
| 46 |
+
print(f"fp16 convert OK in {time.time()-t0:.1f}s")
|
| 47 |
+
mlmodel.save("dense_sam3_trackstep.mlpackage")
|
| 48 |
+
print("saved dense_sam3_trackstep.mlpackage")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
main()
|
tracker/scripts/coreml_traj_test.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trajectory parity: torch wrapper vs converted CoreML model, both from zero
|
| 2 |
+
state, 18 sequential frames with real per-frame inputs. Early frames sit on the
|
| 3 |
+
object-score decision boundary (degenerate zero memory); the test is whether
|
| 4 |
+
late frames track once banks hold real memories.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
import common
|
| 11 |
+
import dense_wrapper as dw
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
import coremltools as ct
|
| 16 |
+
|
| 17 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 18 |
+
wrapper, model = dw.build_wrapper()
|
| 19 |
+
wrapper.mem_bank.zero_(); wrapper.img_bank.zero_(); wrapper.ptr_bank.zero_()
|
| 20 |
+
|
| 21 |
+
mlmodel = ct.models.MLModel("dense_sam3_trackstep.mlpackage",
|
| 22 |
+
compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 23 |
+
in_names = [i.name for i in mlmodel.input_description._fd_spec]
|
| 24 |
+
state = mlmodel.make_state()
|
| 25 |
+
|
| 26 |
+
frames = list(range(16, 34))
|
| 27 |
+
print(f"{'frame':>5} {'osl_rel':>9} {'iou_rel':>9} {'mask_sign_agree':>15} "
|
| 28 |
+
f"{'lowmask_rel':>11}")
|
| 29 |
+
for f in frames:
|
| 30 |
+
inputs = dw.frame_inputs(model, cache, f)
|
| 31 |
+
with torch.no_grad():
|
| 32 |
+
t_low, t_high, t_osl, t_ious = [x.clone() for x in wrapper(*inputs)]
|
| 33 |
+
feed = {n: v.numpy().astype(np.float32)
|
| 34 |
+
for n, v in zip(in_names, inputs)}
|
| 35 |
+
got = mlmodel.predict(feed, state=state)
|
| 36 |
+
# map outputs by shape (names are auto-generated)
|
| 37 |
+
by_shape = {tuple(np.asarray(v).shape): torch.from_numpy(
|
| 38 |
+
np.asarray(v)).float() for v in got.values()}
|
| 39 |
+
c_low = by_shape[tuple(t_low.shape)]
|
| 40 |
+
c_osl = by_shape[tuple(t_osl.shape)]
|
| 41 |
+
c_ious = by_shape[tuple(t_ious.shape)]
|
| 42 |
+
|
| 43 |
+
def rel(a, b):
|
| 44 |
+
return ((a - b).abs().max() / b.abs().max().clamp_min(1e-9)).item()
|
| 45 |
+
|
| 46 |
+
sign_agree = ((c_low > 0) == (t_low > 0)).float().mean().item()
|
| 47 |
+
print(f"{f:>5} {rel(c_osl, t_osl):>9.2e} {rel(c_ious, t_ious):>9.2e} "
|
| 48 |
+
f"{sign_agree:>15.4f} {rel(c_low, t_low):>11.2e}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
main()
|
tracker/scripts/dense_wrapper.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DenseTrackStep: fixed-capacity, dict-free rewrite of SAM3 multiplex track_step
|
| 2 |
+
(propagation-only mode) around the REAL submodules, for torch.export / CoreML.
|
| 3 |
+
|
| 4 |
+
Replaces:
|
| 5 |
+
- output_dict["cond_frame_outputs"/"non_cond_frame_outputs"] dict-walk in
|
| 6 |
+
_prepare_memory_conditioned_features (video_tracking_multiplex.py:1289)
|
| 7 |
+
-> fixed rolling tensor banks (register_buffer) + static concat order
|
| 8 |
+
- MultiplexState add/remove dynamics -> frozen state (1 bucket x multiplex_count);
|
| 9 |
+
its mux/demux matrices become trace-time constants
|
| 10 |
+
- Eager's variable-length skip of missing warmup slots -> fixed banks +
|
| 11 |
+
additive -inf key mask from mem_valid / ptr_valid state buffers
|
| 12 |
+
|
| 13 |
+
Reuses UNCHANGED: transformer.encoder (memory fusion), _forward_sam_heads
|
| 14 |
+
(propagation path incl. MultiplexMaskDecoder), _encode_new_memory
|
| 15 |
+
(SimpleMaskEncoder), all with real weights and real dims
|
| 16 |
+
(hidden_dim=256, num_maskmem=7, 72x72 memory grid, 1008px masks).
|
| 17 |
+
|
| 18 |
+
Scope / documented approximations:
|
| 19 |
+
- Exactly 1 cond frame (the init frame). kino's use-case prompts once.
|
| 20 |
+
- Warmup handled via validity masks (not per-count model variants).
|
| 21 |
+
- Cond maskmem tpos follows use_maskmem_tpos_v2 from frame_pos (idx 6 only
|
| 22 |
+
once frame_idx >= num_maskmem).
|
| 23 |
+
- max_objects = multiplex_count = 16 (released SAM3.1 ckpt).
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn as nn
|
| 28 |
+
|
| 29 |
+
import common
|
| 30 |
+
|
| 31 |
+
E = 72 # sam_image_embedding_size
|
| 32 |
+
HW = E * E # 5184
|
| 33 |
+
C = 256
|
| 34 |
+
NUM_MASKMEM = 7 # 1 cond + 6 rolling non-cond
|
| 35 |
+
N_NONCOND = NUM_MASKMEM - 1
|
| 36 |
+
MAX_PTRS = 16 # 1 cond + 15 rolling non-cond
|
| 37 |
+
N_PTR = MAX_PTRS - 1
|
| 38 |
+
OBJ = common.MULTIPLEX_COUNT # 16
|
| 39 |
+
NUM_OBJ_PTR_TOKENS = MAX_PTRS * OBJ # 256
|
| 40 |
+
# Large finite penalty — CoreML/fp16 friendly vs true -inf
|
| 41 |
+
# Softmax key-pad penalty. Keep modest: -1e4 overflowed coremltools' numpy
|
| 42 |
+
# attention probe during convert and ballooned RAM to ~60GB.
|
| 43 |
+
MASK_NEG = -80.0
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class DenseTrackStep(nn.Module):
|
| 47 |
+
def __init__(self, model, ms, pos72):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.model = model
|
| 50 |
+
self.ms = ms # frozen MultiplexState: matrices are trace-time constants
|
| 51 |
+
self.multimask = model._use_multimask(False, None)
|
| 52 |
+
assert self.multimask, "propagation multimask expected True"
|
| 53 |
+
|
| 54 |
+
# ---- rolling state banks (newest last) ----
|
| 55 |
+
self.register_buffer("mem_bank", torch.zeros(N_NONCOND, 1, C, E, E))
|
| 56 |
+
self.register_buffer("img_bank", torch.zeros(N_NONCOND, HW, 1, C))
|
| 57 |
+
self.register_buffer("ptr_bank", torch.zeros(N_PTR, 1, OBJ, C))
|
| 58 |
+
# 1 = slot filled (attend); 0 = warmup pad (masked)
|
| 59 |
+
self.register_buffer("mem_valid", torch.zeros(N_NONCOND))
|
| 60 |
+
self.register_buffer("ptr_valid", torch.zeros(N_PTR))
|
| 61 |
+
|
| 62 |
+
# ---- constants ----
|
| 63 |
+
with torch.no_grad():
|
| 64 |
+
mm_pos = model.maskmem_backbone.position_encoding(
|
| 65 |
+
torch.zeros(1, C, E, E)
|
| 66 |
+
).to(torch.float32) # (1,C,E,E)
|
| 67 |
+
mm_pos = mm_pos.flatten(2).permute(2, 0, 1) # (HW,1,C)
|
| 68 |
+
tpos = model.maskmem_tpos_enc.detach() # (7,1,1,C)
|
| 69 |
+
# Non-cond bank: slot s -> t_pos=s+1 -> tpos idx 5-s (fixed).
|
| 70 |
+
# Cond tpos is dynamic from frame_pos (see _cond_tpos).
|
| 71 |
+
noncond_mem_pos = []
|
| 72 |
+
noncond_img_pos = []
|
| 73 |
+
for s in range(N_NONCOND):
|
| 74 |
+
idx = N_NONCOND - 1 - s # 5..0
|
| 75 |
+
noncond_mem_pos.append(mm_pos + tpos[idx])
|
| 76 |
+
noncond_img_pos.append(pos72 + tpos[idx])
|
| 77 |
+
self.register_buffer("mm_pos", mm_pos.contiguous())
|
| 78 |
+
self.register_buffer("tpos_enc", tpos.contiguous())
|
| 79 |
+
self.register_buffer(
|
| 80 |
+
"noncond_mem_pos", torch.cat(noncond_mem_pos, dim=0).contiguous()
|
| 81 |
+
)
|
| 82 |
+
self.register_buffer(
|
| 83 |
+
"noncond_img_pos", torch.cat(noncond_img_pos, dim=0).contiguous()
|
| 84 |
+
)
|
| 85 |
+
self.register_buffer("pos72", pos72.clone().contiguous())
|
| 86 |
+
self.register_buffer("ptr_tdiffs", torch.arange(1, MAX_PTRS).float())
|
| 87 |
+
self.register_buffer("_one", torch.ones(1))
|
| 88 |
+
self.register_buffer(
|
| 89 |
+
"_arange7", torch.arange(NUM_MASKMEM, dtype=torch.float32)
|
| 90 |
+
)
|
| 91 |
+
self.register_buffer("_oor_idx", torch.tensor(float(NUM_MASKMEM - 1)))
|
| 92 |
+
|
| 93 |
+
def _obj_pos(self, frame_pos):
|
| 94 |
+
"""Replicates _get_tpos_enc([frame_idx, 1..15], max_abs_pos=16)."""
|
| 95 |
+
from sam3.model.sam3_tracker_utils import get_1d_sine_pe
|
| 96 |
+
|
| 97 |
+
pos = torch.cat([frame_pos.reshape(1), self.ptr_tdiffs]) / (MAX_PTRS - 1)
|
| 98 |
+
pe = get_1d_sine_pe(pos, dim=C) # (16,C)
|
| 99 |
+
pe = self.model.obj_ptr_tpos_proj(pe) # (16,C)
|
| 100 |
+
return pe.unsqueeze(1).repeat_interleave(OBJ, dim=0) # (256,1,C)
|
| 101 |
+
|
| 102 |
+
def _cond_tpos(self, frame_pos):
|
| 103 |
+
"""use_maskmem_tpos_v2 embedding for cond frame at t=0 (t_pos=frame_idx).
|
| 104 |
+
|
| 105 |
+
Soft one-hot over 7 tpos rows — avoids data-dependent integer index
|
| 106 |
+
that torch.export rejects.
|
| 107 |
+
"""
|
| 108 |
+
fp = frame_pos.reshape(()).to(self.tpos_enc.dtype)
|
| 109 |
+
in_range = (NUM_MASKMEM - 1) - torch.clamp(fp, min=0, max=float(N_NONCOND))
|
| 110 |
+
sel = torch.where(fp >= float(NUM_MASKMEM), self._oor_idx, in_range)
|
| 111 |
+
# Exact 1 at integer sel, 0 elsewhere (frame_pos is integral in practice)
|
| 112 |
+
w = (1.0 - (self._arange7 - sel).abs()).clamp(min=0.0)
|
| 113 |
+
return (w.view(NUM_MASKMEM, 1, 1, 1) * self.tpos_enc).sum(dim=0) # (1,1,C)
|
| 114 |
+
|
| 115 |
+
def _mem_img_pos_cat(self, frame_pos):
|
| 116 |
+
"""Cond spatial+tpos (dynamic) then fixed non-cond bank positions."""
|
| 117 |
+
cond_t = self._cond_tpos(frame_pos) # (1,1,C)
|
| 118 |
+
cond_mem_pos = self.mm_pos + cond_t
|
| 119 |
+
cond_img_pos = self.pos72 + cond_t
|
| 120 |
+
return (
|
| 121 |
+
torch.cat([cond_mem_pos, self.noncond_mem_pos], dim=0),
|
| 122 |
+
torch.cat([cond_img_pos, self.noncond_img_pos], dim=0),
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def _memory_key_attn_mask(self):
|
| 126 |
+
"""Additive SDPA mask (1,1,1,K): 0 attend, MASK_NEG block.
|
| 127 |
+
|
| 128 |
+
Key layout matches prompt cat: cond_mem(HW) + mem_bank(N_NONCOND*HW)
|
| 129 |
+
+ cond_ptr(OBJ) + flipped ptr_bank(N_PTR*OBJ).
|
| 130 |
+
"""
|
| 131 |
+
# mem: cond always valid, then per-slot validity expanded over HW
|
| 132 |
+
mem_slot = torch.cat([self._one, self.mem_valid], dim=0) # (7,)
|
| 133 |
+
mem_tok = mem_slot.repeat_interleave(HW) # (7*HW,)
|
| 134 |
+
# ptr: cond always valid; bank is newest-last, prompt uses newest-first
|
| 135 |
+
ptr_slot = torch.cat([self._one, self.ptr_valid.flip(0)], dim=0) # (16,)
|
| 136 |
+
ptr_tok = ptr_slot.repeat_interleave(OBJ) # (16*OBJ,)
|
| 137 |
+
valid = torch.cat([mem_tok, ptr_tok], dim=0) # (K,)
|
| 138 |
+
# (1,1,1,K) broadcasts over batch/heads/queries
|
| 139 |
+
return (MASK_NEG * (1.0 - valid)).view(1, 1, 1, -1)
|
| 140 |
+
|
| 141 |
+
def forward(self, vis72, hires0, hires1, cond_mem, cond_img, cond_ptr,
|
| 142 |
+
frame_pos):
|
| 143 |
+
m = self.model
|
| 144 |
+
|
| 145 |
+
# ---- dense _prepare_memory_conditioned_features ----
|
| 146 |
+
mem_noncond = (
|
| 147 |
+
self.mem_bank.squeeze(1).flatten(2).permute(0, 2, 1)
|
| 148 |
+
.reshape(N_NONCOND * HW, 1, C)
|
| 149 |
+
)
|
| 150 |
+
mem_tokens = torch.cat(
|
| 151 |
+
[cond_mem.flatten(2).permute(2, 0, 1), mem_noncond], dim=0
|
| 152 |
+
) # (7*HW,1,C)
|
| 153 |
+
img_tokens = torch.cat(
|
| 154 |
+
[cond_img, self.img_bank.reshape(N_NONCOND * HW, 1, C)], dim=0
|
| 155 |
+
) # (7*HW,1,C)
|
| 156 |
+
# ptr order: cond, then newest..oldest (t_diff 1..15)
|
| 157 |
+
ptr_noncond = self.ptr_bank.flip(0).squeeze(1).reshape(N_PTR * OBJ, C)
|
| 158 |
+
obj_ptrs = torch.cat(
|
| 159 |
+
[cond_ptr.squeeze(0), ptr_noncond], dim=0
|
| 160 |
+
).unsqueeze(1) # (256,1,C)
|
| 161 |
+
|
| 162 |
+
prompt = torch.cat([mem_tokens, obj_ptrs], dim=0)
|
| 163 |
+
mem_pos_cat, img_pos_cat = self._mem_img_pos_cat(frame_pos)
|
| 164 |
+
prompt_pos = torch.cat([mem_pos_cat, self._obj_pos(frame_pos)], 0)
|
| 165 |
+
key_mask = self._memory_key_attn_mask()
|
| 166 |
+
|
| 167 |
+
enc_out = m.transformer.encoder(
|
| 168 |
+
image=vis72,
|
| 169 |
+
src=vis72,
|
| 170 |
+
memory_image=img_tokens,
|
| 171 |
+
memory=prompt,
|
| 172 |
+
image_pos=self.pos72,
|
| 173 |
+
src_pos=self.pos72,
|
| 174 |
+
memory_image_pos=img_pos_cat,
|
| 175 |
+
memory_pos=prompt_pos,
|
| 176 |
+
num_obj_ptr_tokens=NUM_OBJ_PTR_TOKENS,
|
| 177 |
+
memory_key_attn_mask=key_mask,
|
| 178 |
+
)
|
| 179 |
+
pix_feat_with_mem = enc_out["memory"].permute(1, 2, 0).view(1, C, E, E)
|
| 180 |
+
|
| 181 |
+
# ---- REAL sam heads (propagation path) ----
|
| 182 |
+
sam_out = m._forward_sam_heads(
|
| 183 |
+
backbone_features=pix_feat_with_mem,
|
| 184 |
+
propagation_high_res_features=[hires0, hires1],
|
| 185 |
+
multimask_output=self.multimask,
|
| 186 |
+
objects_to_interact=list(range(OBJ)),
|
| 187 |
+
multiplex_state=self.ms,
|
| 188 |
+
)
|
| 189 |
+
low_res_masks = sam_out["low_res_masks"]
|
| 190 |
+
high_res_masks = sam_out["high_res_masks"]
|
| 191 |
+
object_score_logits = sam_out["object_score_logits"]
|
| 192 |
+
ious = sam_out["ious"]
|
| 193 |
+
obj_ptr = sam_out["obj_ptr"]
|
| 194 |
+
|
| 195 |
+
# ---- REAL memory encoder ----
|
| 196 |
+
maskmem_features, _ = m._encode_new_memory(
|
| 197 |
+
image=None,
|
| 198 |
+
current_vision_feats=[vis72],
|
| 199 |
+
feat_sizes=[(E, E)],
|
| 200 |
+
pred_masks_high_res=high_res_masks,
|
| 201 |
+
object_score_logits=object_score_logits,
|
| 202 |
+
is_mask_from_pts=False,
|
| 203 |
+
conditioning_objects=set(),
|
| 204 |
+
multiplex_state=self.ms,
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# ---- state update: shift + append (static shapes) ----
|
| 208 |
+
self.mem_bank.copy_(
|
| 209 |
+
torch.cat([self.mem_bank[1:], maskmem_features.unsqueeze(0)], 0)
|
| 210 |
+
)
|
| 211 |
+
self.img_bank.copy_(
|
| 212 |
+
torch.cat([self.img_bank[1:], vis72.unsqueeze(0)], 0)
|
| 213 |
+
)
|
| 214 |
+
mux_ptr = self.ms.mux(obj_ptr) # (1,OBJ,C)
|
| 215 |
+
self.ptr_bank.copy_(
|
| 216 |
+
torch.cat([self.ptr_bank[1:], mux_ptr.unsqueeze(0)], 0)
|
| 217 |
+
)
|
| 218 |
+
self.mem_valid.copy_(torch.cat([self.mem_valid[1:], self._one], 0))
|
| 219 |
+
self.ptr_valid.copy_(torch.cat([self.ptr_valid[1:], self._one], 0))
|
| 220 |
+
|
| 221 |
+
return low_res_masks, high_res_masks, object_score_logits, ious
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def build_wrapper():
|
| 225 |
+
model = common.build_model()
|
| 226 |
+
pos72 = common.pos_embed_72(model)
|
| 227 |
+
ms = common.make_multiplex_state(model)
|
| 228 |
+
return DenseTrackStep(model, ms, pos72).eval(), model
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def seed_banks(wrapper, cache):
|
| 232 |
+
"""Fill banks from eager frames so state matches eager just before TARGET."""
|
| 233 |
+
tgt = cache["target_frame"]
|
| 234 |
+
frames = cache["frames"]
|
| 235 |
+
for s in range(N_NONCOND): # slot s <- frame tgt-6+s
|
| 236 |
+
f = tgt - N_NONCOND + s
|
| 237 |
+
wrapper.mem_bank[s] = frames[f]["maskmem_features"]
|
| 238 |
+
wrapper.img_bank[s] = frames[f]["image_features"]
|
| 239 |
+
wrapper.mem_valid[s] = 1.0
|
| 240 |
+
for p in range(N_PTR): # slot p <- frame tgt-15+p
|
| 241 |
+
f = tgt - N_PTR + p
|
| 242 |
+
wrapper.ptr_bank[p] = frames[f]["obj_ptr"]
|
| 243 |
+
wrapper.ptr_valid[p] = 1.0
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def seed_banks_for_frame(wrapper, cache, frame_idx):
|
| 247 |
+
"""Seed banks to match eager state *entering* frame_idx (warmup-aware)."""
|
| 248 |
+
wrapper.mem_bank.zero_()
|
| 249 |
+
wrapper.img_bank.zero_()
|
| 250 |
+
wrapper.ptr_bank.zero_()
|
| 251 |
+
wrapper.mem_valid.zero_()
|
| 252 |
+
wrapper.ptr_valid.zero_()
|
| 253 |
+
frames = cache["frames"]
|
| 254 |
+
# mem: last up to N_NONCOND non-cond frames before frame_idx
|
| 255 |
+
mem_src = [f for f in range(max(1, frame_idx - N_NONCOND), frame_idx)]
|
| 256 |
+
for i, f in enumerate(mem_src):
|
| 257 |
+
slot = N_NONCOND - len(mem_src) + i
|
| 258 |
+
wrapper.mem_bank[slot] = frames[f]["maskmem_features"]
|
| 259 |
+
wrapper.img_bank[slot] = frames[f]["image_features"]
|
| 260 |
+
wrapper.mem_valid[slot] = 1.0
|
| 261 |
+
ptr_src = [f for f in range(max(1, frame_idx - N_PTR), frame_idx)]
|
| 262 |
+
for i, f in enumerate(ptr_src):
|
| 263 |
+
slot = N_PTR - len(ptr_src) + i
|
| 264 |
+
wrapper.ptr_bank[slot] = frames[f]["obj_ptr"]
|
| 265 |
+
wrapper.ptr_valid[slot] = 1.0
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def frame_inputs(model, cache, frame_idx):
|
| 269 |
+
feats, _ = common.synth_frame_features(model, frame_idx)
|
| 270 |
+
vis72 = feats[2].contiguous()
|
| 271 |
+
hires0 = feats[0].permute(1, 2, 0).view(1, 32, 4 * E, 4 * E).contiguous()
|
| 272 |
+
hires1 = feats[1].permute(1, 2, 0).view(1, 64, 2 * E, 2 * E).contiguous()
|
| 273 |
+
cond = cache["cond"]
|
| 274 |
+
return (vis72, hires0, hires1,
|
| 275 |
+
cond["maskmem_features"].contiguous(),
|
| 276 |
+
cond["image_features"].contiguous(),
|
| 277 |
+
cond["obj_ptr"].contiguous(),
|
| 278 |
+
torch.tensor([float(frame_idx)]))
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def main():
|
| 282 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 283 |
+
wrapper, model = build_wrapper()
|
| 284 |
+
|
| 285 |
+
def report(name, a, b):
|
| 286 |
+
d = (a - b).abs().max().item()
|
| 287 |
+
r = b.abs().max().item()
|
| 288 |
+
print(f" {name:24s} max|diff|={d:.3e} (ref max|val|={r:.3e})")
|
| 289 |
+
return d
|
| 290 |
+
|
| 291 |
+
# ---- steady-state parity ----
|
| 292 |
+
seed_banks(wrapper, cache)
|
| 293 |
+
tgt = cache["target_frame"]
|
| 294 |
+
inputs = frame_inputs(model, cache, tgt)
|
| 295 |
+
with torch.no_grad():
|
| 296 |
+
low, high, osl, ious = wrapper(*inputs)
|
| 297 |
+
t = cache["target"]
|
| 298 |
+
print(f"parity vs eager track_step @ frame {tgt} (steady):")
|
| 299 |
+
d1 = report("pred_masks(low)", low, t["pred_masks"])
|
| 300 |
+
d2 = report("pred_masks_high_res", high, t["pred_masks_high_res"])
|
| 301 |
+
d3 = report("object_score_logits", osl, t["object_score_logits"])
|
| 302 |
+
d4 = report("ious", ious, t["ious"])
|
| 303 |
+
d5 = report("new maskmem_features", wrapper.mem_bank[-1],
|
| 304 |
+
t["maskmem_features"].squeeze(0))
|
| 305 |
+
d6 = report("new obj_ptr (muxed)", wrapper.ptr_bank[-1], t["obj_ptr"])
|
| 306 |
+
ok_steady = max(d1, d2, d3, d4, d5, d6) < 1e-3
|
| 307 |
+
|
| 308 |
+
# ---- warmup parity (frame 3) ----
|
| 309 |
+
warm = cache.get("warmup_frame", 3)
|
| 310 |
+
if "warmup" in cache:
|
| 311 |
+
seed_banks_for_frame(wrapper, cache, warm)
|
| 312 |
+
with torch.no_grad():
|
| 313 |
+
low, high, osl, ious = wrapper(*frame_inputs(model, cache, warm))
|
| 314 |
+
w = cache["warmup"]
|
| 315 |
+
print(f"parity vs eager track_step @ frame {warm} (warmup):")
|
| 316 |
+
w1 = report("pred_masks(low)", low, w["pred_masks"])
|
| 317 |
+
w2 = report("pred_masks_high_res", high, w["pred_masks_high_res"])
|
| 318 |
+
w3 = report("object_score_logits", osl, w["object_score_logits"])
|
| 319 |
+
w4 = report("ious", ious, w["ious"])
|
| 320 |
+
ok_warm = max(w1, w2, w3, w4) < 1e-3
|
| 321 |
+
else:
|
| 322 |
+
print("warmup cache missing — skip warmup check")
|
| 323 |
+
ok_warm = True
|
| 324 |
+
|
| 325 |
+
print("PARITY:", "PASS" if (ok_steady and ok_warm) else "FAIL")
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
if __name__ == "__main__":
|
| 329 |
+
main()
|
tracker/scripts/eager_baseline.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Eager SAM3 track_step baseline: frame 0 mask-init + frames 1..TARGET propagation.
|
| 2 |
+
Caches per-frame outputs needed by the dense wrapper + TARGET/warmup eager
|
| 3 |
+
outputs for parity checking. One-time cost; results go to eager_cache.pt.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
import torch
|
| 8 |
+
import common
|
| 9 |
+
|
| 10 |
+
TARGET = 16 # first frame with FULL banks: mem needs f-1..f-6, ptrs need f-1..f-15
|
| 11 |
+
WARMUP = 3 # partial banks — validates validity-mask path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def bf_prop(model, frame_idx):
|
| 15 |
+
feats, sizes = common.synth_frame_features(model, frame_idx)
|
| 16 |
+
pos72 = common.POS72
|
| 17 |
+
return {
|
| 18 |
+
"vision_feats": feats,
|
| 19 |
+
"vision_masks": [None, None, None],
|
| 20 |
+
"vision_pos_embeds": [None, None, pos72],
|
| 21 |
+
"feat_sizes": sizes,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def bf_inter(model, frame_idx):
|
| 26 |
+
feats, sizes = common.synth_frame_features(model, frame_idx)
|
| 27 |
+
return {"vision_feats": feats, "feat_sizes": sizes}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main():
|
| 31 |
+
model = common.build_model()
|
| 32 |
+
common.POS72 = common.pos_embed_72(model)
|
| 33 |
+
ms = common.make_multiplex_state(model)
|
| 34 |
+
output_dict = {"cond_frame_outputs": {}, "non_cond_frame_outputs": {}}
|
| 35 |
+
|
| 36 |
+
with torch.no_grad():
|
| 37 |
+
# ---- frame 0: init via mask input (mode: mask_as_output) ----
|
| 38 |
+
t0 = time.time()
|
| 39 |
+
out0 = model.track_step(
|
| 40 |
+
frame_idx=0,
|
| 41 |
+
is_init_cond_frame=True,
|
| 42 |
+
backbone_features_interactive=bf_inter(model, 0),
|
| 43 |
+
backbone_features_propagation=bf_prop(model, 0),
|
| 44 |
+
image=None,
|
| 45 |
+
point_inputs=None,
|
| 46 |
+
mask_inputs=common.init_masks(),
|
| 47 |
+
gt_masks=None,
|
| 48 |
+
frames_to_add_correction_pt=[],
|
| 49 |
+
output_dict=output_dict,
|
| 50 |
+
num_frames=64,
|
| 51 |
+
multiplex_state=ms,
|
| 52 |
+
)
|
| 53 |
+
output_dict["cond_frame_outputs"][0] = out0
|
| 54 |
+
print(f"frame 0 (init): {time.time()-t0:.1f}s")
|
| 55 |
+
for k, v in out0.items():
|
| 56 |
+
if torch.is_tensor(v):
|
| 57 |
+
print(f" {k}: {tuple(v.shape)} {v.dtype}")
|
| 58 |
+
elif isinstance(v, list) and v and torch.is_tensor(v[0]):
|
| 59 |
+
print(f" {k}: list[{tuple(v[0].shape)}]")
|
| 60 |
+
else:
|
| 61 |
+
print(f" {k}: {type(v).__name__} {v if isinstance(v, set) else ''}")
|
| 62 |
+
|
| 63 |
+
# ---- frames 1..TARGET: propagation only ----
|
| 64 |
+
for f in range(1, TARGET + 1):
|
| 65 |
+
t0 = time.time()
|
| 66 |
+
out = model.track_step(
|
| 67 |
+
frame_idx=f,
|
| 68 |
+
is_init_cond_frame=False,
|
| 69 |
+
backbone_features_interactive=None,
|
| 70 |
+
backbone_features_propagation=bf_prop(model, f),
|
| 71 |
+
image=None,
|
| 72 |
+
point_inputs=None,
|
| 73 |
+
mask_inputs=None,
|
| 74 |
+
gt_masks=None,
|
| 75 |
+
frames_to_add_correction_pt=[],
|
| 76 |
+
output_dict=output_dict,
|
| 77 |
+
num_frames=64,
|
| 78 |
+
multiplex_state=ms,
|
| 79 |
+
)
|
| 80 |
+
output_dict["non_cond_frame_outputs"][f] = out
|
| 81 |
+
print(f"frame {f}: {time.time()-t0:.1f}s "
|
| 82 |
+
f"maskmem {tuple(out['maskmem_features'].shape)} "
|
| 83 |
+
f"obj_ptr {tuple(out['obj_ptr'].shape)}")
|
| 84 |
+
|
| 85 |
+
# ---- cache what the dense wrapper needs ----
|
| 86 |
+
keep = ["maskmem_features", "maskmem_pos_enc", "obj_ptr",
|
| 87 |
+
"image_features", "image_pos_enc", "object_score_logits"]
|
| 88 |
+
cache = {"cond": {}, "frames": {}, "target": {}, "warmup": {}}
|
| 89 |
+
for k in keep:
|
| 90 |
+
cache["cond"][k] = out0.get(k)
|
| 91 |
+
for f in range(1, TARGET):
|
| 92 |
+
cache["frames"][f] = {k: output_dict["non_cond_frame_outputs"][f].get(k)
|
| 93 |
+
for k in keep}
|
| 94 |
+
|
| 95 |
+
def pack_target(frame_idx, bucket):
|
| 96 |
+
tgt = output_dict["non_cond_frame_outputs"][frame_idx]
|
| 97 |
+
for k in ["pred_masks", "pred_masks_high_res", "object_score_logits",
|
| 98 |
+
"maskmem_features", "obj_ptr"]:
|
| 99 |
+
bucket[k] = tgt[k]
|
| 100 |
+
bucket["ious"] = tgt["multistep_pred_ious"][-1]
|
| 101 |
+
|
| 102 |
+
pack_target(TARGET, cache["target"])
|
| 103 |
+
cache["target_frame"] = TARGET
|
| 104 |
+
pack_target(WARMUP, cache["warmup"])
|
| 105 |
+
cache["warmup_frame"] = WARMUP
|
| 106 |
+
|
| 107 |
+
torch.save(cache, "eager_cache.pt")
|
| 108 |
+
print("saved eager_cache.pt")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
if __name__ == "__main__":
|
| 112 |
+
main()
|
tracker/scripts/export_coreml.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""torch.export -> coremltools convert (fp16 default) of DenseTrackStep,
|
| 2 |
+
then CoreML-vs-torch parity over warmup→steady stateful frames.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import time
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
import common
|
| 10 |
+
import dense_wrapper as dw
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def zero_state(wrapper):
|
| 14 |
+
wrapper.mem_bank.zero_()
|
| 15 |
+
wrapper.img_bank.zero_()
|
| 16 |
+
wrapper.ptr_bank.zero_()
|
| 17 |
+
wrapper.mem_valid.zero_()
|
| 18 |
+
wrapper.ptr_valid.zero_()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 23 |
+
wrapper, model = dw.build_wrapper()
|
| 24 |
+
inputs = dw.frame_inputs(model, cache, 3)
|
| 25 |
+
|
| 26 |
+
common.hide_triton_stub()
|
| 27 |
+
t0 = time.time()
|
| 28 |
+
with torch.no_grad():
|
| 29 |
+
try:
|
| 30 |
+
ep = torch.export.export(wrapper, inputs)
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"strict export failed ({type(e).__name__}: {e}); retrying strict=False")
|
| 33 |
+
ep = torch.export.export(wrapper, inputs, strict=False)
|
| 34 |
+
ep = ep.run_decompositions({})
|
| 35 |
+
print(f"torch.export + decompositions OK in {time.time()-t0:.1f}s")
|
| 36 |
+
|
| 37 |
+
import coremltools as ct
|
| 38 |
+
from coremltools.converters.mil.frontend.torch.torch_op_registry import (
|
| 39 |
+
register_torch_op,
|
| 40 |
+
)
|
| 41 |
+
from coremltools.converters.mil.frontend.torch.ops import _get_inputs
|
| 42 |
+
from coremltools.converters.mil.mil import Builder as mb
|
| 43 |
+
|
| 44 |
+
@register_torch_op(torch_alias=["where.scalarother"])
|
| 45 |
+
def where_scalarother(context, node):
|
| 46 |
+
cond, a, b = _get_inputs(context=context, node=node, expected=3)
|
| 47 |
+
context.add(mb.select(cond=cond, a=a, b=b), node.name)
|
| 48 |
+
|
| 49 |
+
t0 = time.time()
|
| 50 |
+
mlmodel = ct.convert(
|
| 51 |
+
ep,
|
| 52 |
+
minimum_deployment_target=ct.target.iOS18,
|
| 53 |
+
compute_units=ct.ComputeUnit.CPU_AND_GPU,
|
| 54 |
+
)
|
| 55 |
+
print(f"coremltools convert OK in {time.time()-t0:.1f}s")
|
| 56 |
+
mlmodel.save("dense_sam3_trackstep.mlpackage")
|
| 57 |
+
print("saved dense_sam3_trackstep.mlpackage")
|
| 58 |
+
|
| 59 |
+
zero_state(wrapper)
|
| 60 |
+
frames = list(range(1, 19))
|
| 61 |
+
torch_outs = []
|
| 62 |
+
with torch.no_grad():
|
| 63 |
+
for f in frames:
|
| 64 |
+
torch_outs.append([t.clone() for t in wrapper(*dw.frame_inputs(model, cache, f))])
|
| 65 |
+
|
| 66 |
+
in_names = [i.name for i in mlmodel.input_description._fd_spec]
|
| 67 |
+
out_names = [o.name for o in mlmodel.output_description._fd_spec]
|
| 68 |
+
print("inputs:", in_names)
|
| 69 |
+
print("outputs:", out_names)
|
| 70 |
+
print(f"{'frame':>5} {'osl_rel':>9} {'iou_rel':>9} {'sign_agree':>10} {'low_rel':>9}")
|
| 71 |
+
|
| 72 |
+
state = mlmodel.make_state()
|
| 73 |
+
worst = 0.0
|
| 74 |
+
appear_flips = 0
|
| 75 |
+
for i, f in enumerate(frames):
|
| 76 |
+
feed = {n: v.numpy().astype(np.float32)
|
| 77 |
+
for n, v in zip(in_names, dw.frame_inputs(model, cache, f))}
|
| 78 |
+
got = mlmodel.predict(feed, state=state)
|
| 79 |
+
by_shape = {tuple(np.asarray(v).shape): torch.from_numpy(
|
| 80 |
+
np.asarray(v)).float() for v in got.values()}
|
| 81 |
+
t_low, t_high, t_osl, t_ious = torch_outs[i]
|
| 82 |
+
c_low = by_shape[tuple(t_low.shape)]
|
| 83 |
+
c_osl = by_shape[tuple(t_osl.shape)]
|
| 84 |
+
c_ious = by_shape[tuple(t_ious.shape)]
|
| 85 |
+
|
| 86 |
+
def rel(a, b):
|
| 87 |
+
return ((a - b).abs().max() / b.abs().max().clamp_min(1e-9)).item()
|
| 88 |
+
|
| 89 |
+
sign = ((c_low > 0) == (t_low > 0)).float().mean().item()
|
| 90 |
+
r_osl, r_iou, r_low = rel(c_osl, t_osl), rel(c_ious, t_ious), rel(c_low, t_low)
|
| 91 |
+
worst = max(worst, r_osl, r_iou, r_low)
|
| 92 |
+
# is_obj_appearing threshold at 0
|
| 93 |
+
if not torch.equal((c_osl > 0), (t_osl > 0)):
|
| 94 |
+
appear_flips += 1
|
| 95 |
+
print(f"{f:>5} {r_osl:>9.2e} {r_iou:>9.2e} {sign:>10.4f} {r_low:>9.2e}")
|
| 96 |
+
|
| 97 |
+
print(f"worst rel: {worst:.3e} appear_flips: {appear_flips}/{len(frames)}")
|
| 98 |
+
print("FP16 TRAJ:", "PASS" if worst < 5e-2 and appear_flips == 0 else "CHECK")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
main()
|
tracker/scripts/export_fp32.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FLOAT32 CoreML convert + trajectory parity from zero state (warmup→steady)."""
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
import common
|
| 8 |
+
import dense_wrapper as dw
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def zero_state(wrapper):
|
| 12 |
+
wrapper.mem_bank.zero_()
|
| 13 |
+
wrapper.img_bank.zero_()
|
| 14 |
+
wrapper.ptr_bank.zero_()
|
| 15 |
+
wrapper.mem_valid.zero_()
|
| 16 |
+
wrapper.ptr_valid.zero_()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 21 |
+
wrapper, model = dw.build_wrapper()
|
| 22 |
+
# Export at a mid frame so frame_pos path (dynamic cond tpos) is exercised
|
| 23 |
+
inputs = dw.frame_inputs(model, cache, 3)
|
| 24 |
+
|
| 25 |
+
common.hide_triton_stub()
|
| 26 |
+
with torch.no_grad():
|
| 27 |
+
try:
|
| 28 |
+
ep = torch.export.export(wrapper, inputs)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"strict export failed ({type(e).__name__}: {e}); retrying strict=False")
|
| 31 |
+
ep = torch.export.export(wrapper, inputs, strict=False)
|
| 32 |
+
ep = ep.run_decompositions({})
|
| 33 |
+
print("export OK")
|
| 34 |
+
|
| 35 |
+
import coremltools as ct
|
| 36 |
+
from coremltools.converters.mil.frontend.torch.torch_op_registry import (
|
| 37 |
+
register_torch_op,
|
| 38 |
+
)
|
| 39 |
+
from coremltools.converters.mil.frontend.torch.ops import _get_inputs
|
| 40 |
+
from coremltools.converters.mil.mil import Builder as mb
|
| 41 |
+
|
| 42 |
+
@register_torch_op(torch_alias=["where.scalarother"])
|
| 43 |
+
def where_scalarother(context, node):
|
| 44 |
+
cond, a, b = _get_inputs(context=context, node=node, expected=3)
|
| 45 |
+
context.add(mb.select(cond=cond, a=a, b=b), node.name)
|
| 46 |
+
|
| 47 |
+
t0 = time.time()
|
| 48 |
+
mlmodel = ct.convert(
|
| 49 |
+
ep,
|
| 50 |
+
minimum_deployment_target=ct.target.iOS18,
|
| 51 |
+
# Convert/load on CPU; GPU JIT of masked SDPA previously ballooned RAM.
|
| 52 |
+
# Deploy path still CPU_AND_GPU via MLModel(..., compute_units=...).
|
| 53 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 54 |
+
compute_precision=ct.precision.FLOAT32,
|
| 55 |
+
)
|
| 56 |
+
print(f"fp32 convert OK in {time.time()-t0:.1f}s")
|
| 57 |
+
mlmodel.save("dense_sam3_trackstep_fp32.mlpackage")
|
| 58 |
+
|
| 59 |
+
zero_state(wrapper)
|
| 60 |
+
in_names = [i.name for i in mlmodel.input_description._fd_spec]
|
| 61 |
+
state = mlmodel.make_state()
|
| 62 |
+
print(f"{'frame':>5} {'osl_rel':>9} {'iou_rel':>9} {'sign_agree':>10} "
|
| 63 |
+
f"{'lowmask_rel':>11} {'t_nan':>5} {'c_nan':>5}")
|
| 64 |
+
worst = 0.0
|
| 65 |
+
for f in range(1, 11): # warmup→near-steady; enough to prove state+mask
|
| 66 |
+
fi = dw.frame_inputs(model, cache, f)
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
t_low, t_high, t_osl, t_ious = [x.clone() for x in wrapper(*fi)]
|
| 69 |
+
feed = {n: v.numpy().astype(np.float32) for n, v in zip(in_names, fi)}
|
| 70 |
+
got = mlmodel.predict(feed, state=state)
|
| 71 |
+
by_shape = {tuple(np.asarray(v).shape): torch.from_numpy(
|
| 72 |
+
np.asarray(v)).float() for v in got.values()}
|
| 73 |
+
c_low = by_shape[tuple(t_low.shape)]
|
| 74 |
+
c_osl = by_shape[tuple(t_osl.shape)]
|
| 75 |
+
c_ious = by_shape[tuple(t_ious.shape)]
|
| 76 |
+
|
| 77 |
+
def rel(a, b):
|
| 78 |
+
return ((a - b).abs().max() / b.abs().max().clamp_min(1e-9)).item()
|
| 79 |
+
|
| 80 |
+
sign = ((c_low > 0) == (t_low > 0)).float().mean().item()
|
| 81 |
+
r_osl, r_iou, r_low = rel(c_osl, t_osl), rel(c_ious, t_ious), rel(c_low, t_low)
|
| 82 |
+
worst = max(worst, r_osl, r_iou, r_low)
|
| 83 |
+
print(f"{f:>5} {r_osl:>9.2e} {r_iou:>9.2e} "
|
| 84 |
+
f"{sign:>10.4f} {r_low:>11.2e} "
|
| 85 |
+
f"{int(t_low.isnan().sum()):>5} {int(c_low.isnan().sum()):>5}")
|
| 86 |
+
print(f"worst rel: {worst:.3e}")
|
| 87 |
+
print("FP32 TRAJ:", "PASS" if worst < 1e-4 else "CHECK")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
main()
|
tracker/scripts/test_cross_state.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Repro of prior spike's crasher: 2 cross-dependent states + attention.
|
| 2 |
+
State A (mem_bank) is READ -> attention -> result WRITTEN to state B (ptr_bank).
|
| 3 |
+
Prior result on torch 2.13 + coremltools 9.0: KeyError in optimize_state.py:93.
|
| 4 |
+
Now testing under torch 2.7.0 (coremltools' tested ceiling).
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import coremltools as ct
|
| 11 |
+
|
| 12 |
+
C, T, S = 256, 7, 64 # hidden dim, num_maskmem, spatial tokens
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class CrossStateModel(nn.Module):
|
| 16 |
+
def __init__(self):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.register_buffer("mem_bank", torch.zeros(T, S, C)) # state A
|
| 19 |
+
self.register_buffer("ptr_bank", torch.zeros(T, C)) # state B
|
| 20 |
+
self.attn = nn.MultiheadAttention(C, 4, batch_first=True)
|
| 21 |
+
self.enc = nn.Linear(C, C)
|
| 22 |
+
|
| 23 |
+
def forward(self, x): # x: (1, S, C)
|
| 24 |
+
# read BOTH states
|
| 25 |
+
mem = self.mem_bank.reshape(1, T * S, C)
|
| 26 |
+
ptrs = self.ptr_bank.reshape(1, T, C)
|
| 27 |
+
kv = torch.cat([mem, ptrs], dim=1)
|
| 28 |
+
y, _ = self.attn(x, kv, kv)
|
| 29 |
+
# write state B from a value derived from reading state A (cross-dependent)
|
| 30 |
+
new_ptr = y.mean(dim=1) # (1, C)
|
| 31 |
+
ptr_updated = torch.cat([self.ptr_bank[1:], new_ptr], dim=0)
|
| 32 |
+
self.ptr_bank.copy_(ptr_updated)
|
| 33 |
+
# write state A from current input (shift + cat, static shape)
|
| 34 |
+
new_mem = self.enc(y) # (1, S, C)
|
| 35 |
+
mem_updated = torch.cat([self.mem_bank[1:], new_mem], dim=0)
|
| 36 |
+
self.mem_bank.copy_(mem_updated)
|
| 37 |
+
return y
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
torch.manual_seed(0)
|
| 42 |
+
m = CrossStateModel().eval()
|
| 43 |
+
m.requires_grad_(False)
|
| 44 |
+
x = torch.randn(1, S, C)
|
| 45 |
+
|
| 46 |
+
with torch.no_grad():
|
| 47 |
+
ep = torch.export.export(m, (x,))
|
| 48 |
+
ep = ep.run_decompositions({})
|
| 49 |
+
print("torch.export OK")
|
| 50 |
+
|
| 51 |
+
mlmodel = ct.convert(
|
| 52 |
+
ep,
|
| 53 |
+
minimum_deployment_target=ct.target.iOS18,
|
| 54 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 55 |
+
)
|
| 56 |
+
print("coremltools convert OK")
|
| 57 |
+
|
| 58 |
+
state = mlmodel.make_state()
|
| 59 |
+
out1 = mlmodel.predict({"x": x.numpy()}, state=state)
|
| 60 |
+
out2 = mlmodel.predict({"x": x.numpy()}, state=state)
|
| 61 |
+
k = list(out1)[0]
|
| 62 |
+
print("predict OK; outputs differ across calls (state evolving):",
|
| 63 |
+
not np.allclose(out1[k], out2[k]))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
tracker/scripts/toy_ane_test.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Does ANY stateful attention model ANE-compile on this machine?
|
| 2 |
+
Reuses the cross-state toy from test_cross_state.py at small scale."""
|
| 3 |
+
import numpy as np, torch, sys
|
| 4 |
+
import coremltools as ct
|
| 5 |
+
from test_cross_state import CrossStateModel, C, S
|
| 6 |
+
|
| 7 |
+
torch.manual_seed(0)
|
| 8 |
+
m = CrossStateModel().eval(); m.requires_grad_(False)
|
| 9 |
+
x = torch.randn(1, S, C)
|
| 10 |
+
with torch.no_grad():
|
| 11 |
+
ep = torch.export.export(m, (x,)); ep = ep.run_decompositions({})
|
| 12 |
+
unit = getattr(ct.ComputeUnit, sys.argv[1] if len(sys.argv) > 1 else "CPU_AND_NE")
|
| 13 |
+
ml = ct.convert(ep, minimum_deployment_target=ct.target.iOS18, compute_units=unit)
|
| 14 |
+
st = ml.make_state()
|
| 15 |
+
o1 = ml.predict({"x": x.numpy()}, state=st); o2 = ml.predict({"x": x.numpy()}, state=st)
|
| 16 |
+
k = list(o1)[0]
|
| 17 |
+
print("toy stateful attn on", unit, "OK; state evolves:", not np.allclose(o1[k], o2[k]))
|
tracker/scripts/toy_bisect.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np, torch, torch.nn as nn, sys
|
| 2 |
+
import coremltools as ct
|
| 3 |
+
|
| 4 |
+
C, S, T = 256, 64, 7
|
| 5 |
+
torch.manual_seed(0)
|
| 6 |
+
|
| 7 |
+
class Stateless(nn.Module):
|
| 8 |
+
def __init__(self):
|
| 9 |
+
super().__init__()
|
| 10 |
+
self.attn = nn.MultiheadAttention(C, 4, batch_first=True)
|
| 11 |
+
self.enc = nn.Linear(C, C)
|
| 12 |
+
def forward(self, x):
|
| 13 |
+
y, _ = self.attn(x, x, x)
|
| 14 |
+
return self.enc(y)
|
| 15 |
+
|
| 16 |
+
class StateNoAttn(nn.Module):
|
| 17 |
+
def __init__(self):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.register_buffer("bank", torch.zeros(T, S, C))
|
| 20 |
+
self.enc = nn.Linear(C, C)
|
| 21 |
+
def forward(self, x):
|
| 22 |
+
y = self.enc(x) + self.bank.mean(dim=0, keepdim=True)
|
| 23 |
+
self.bank.copy_(torch.cat([self.bank[1:], y], 0))
|
| 24 |
+
return y
|
| 25 |
+
|
| 26 |
+
which = sys.argv[1]
|
| 27 |
+
m = {"stateless": Stateless, "state_noattn": StateNoAttn}[which]().eval()
|
| 28 |
+
m.requires_grad_(False)
|
| 29 |
+
x = torch.randn(1, S, C)
|
| 30 |
+
with torch.no_grad():
|
| 31 |
+
ep = torch.export.export(m, (x,)); ep = ep.run_decompositions({})
|
| 32 |
+
ml = ct.convert(ep, minimum_deployment_target=ct.target.iOS18,
|
| 33 |
+
compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 34 |
+
st = ml.make_state() if which != "stateless" else None
|
| 35 |
+
kw = {"state": st} if st is not None else {}
|
| 36 |
+
o = ml.predict({"x": x.numpy()}, **kw)
|
| 37 |
+
print(which, "on CPU_AND_NE: predict OK")
|
tracker/scripts/toy_minstate.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np, torch, torch.nn as nn
|
| 2 |
+
import coremltools as ct
|
| 3 |
+
|
| 4 |
+
torch.manual_seed(0)
|
| 5 |
+
class MinState(nn.Module):
|
| 6 |
+
def __init__(self):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.register_buffer("bank", torch.zeros(1, 64, 256)) # all dims 32-friendly
|
| 9 |
+
self.enc = nn.Linear(256, 256)
|
| 10 |
+
def forward(self, x): # (1,64,256)
|
| 11 |
+
y = self.enc(x + self.bank)
|
| 12 |
+
self.bank.copy_(y) # whole-buffer update, no slice/cat
|
| 13 |
+
return y
|
| 14 |
+
|
| 15 |
+
m = MinState().eval(); m.requires_grad_(False)
|
| 16 |
+
x = torch.randn(1, 64, 256)
|
| 17 |
+
with torch.no_grad():
|
| 18 |
+
ep = torch.export.export(m, (x,)); ep = ep.run_decompositions({})
|
| 19 |
+
ml = ct.convert(ep, minimum_deployment_target=ct.target.iOS18,
|
| 20 |
+
compute_units=ct.ComputeUnit.CPU_AND_NE)
|
| 21 |
+
st = ml.make_state()
|
| 22 |
+
o = ml.predict({"x": x.numpy()}, state=st)
|
| 23 |
+
print("minimal aligned whole-buffer state on CPU_AND_NE: predict OK")
|
tracker/scripts/verify_coreml_lean.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Low-RAM CoreML runtime parity: load a saved mlpackage, drive it statefully
|
| 2 |
+
frames 1..16 from zero state, compare CoreML outputs to the CACHED eager
|
| 3 |
+
targets (warmup f3, steady f16). No torch-eager forward in the loop and no
|
| 4 |
+
checkpoint load (synth inputs are weight-independent), so peak RAM stays well
|
| 5 |
+
under the 16GB that the in-process double-run in export_fp32.py blows past.
|
| 6 |
+
|
| 7 |
+
Usage: python verify_coreml_lean.py <mlpackage> [compute_units]
|
| 8 |
+
compute_units: CPU_ONLY (default) | CPU_AND_GPU
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import sys
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
import common
|
| 16 |
+
import dense_wrapper as dw
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
pkg = sys.argv[1] if len(sys.argv) > 1 else "dense_sam3_trackstep_fp32.mlpackage"
|
| 21 |
+
cu = sys.argv[2] if len(sys.argv) > 2 else "CPU_ONLY"
|
| 22 |
+
|
| 23 |
+
cache = torch.load("eager_cache.pt", weights_only=False)
|
| 24 |
+
# Random-weight model ONLY for weight-independent input generation
|
| 25 |
+
# (synth features + embedding size). Avoids a second 457-tensor load.
|
| 26 |
+
model = common.build_model(load_checkpoint=False)
|
| 27 |
+
|
| 28 |
+
import coremltools as ct
|
| 29 |
+
mlmodel = ct.models.MLModel(pkg, compute_units=getattr(ct.ComputeUnit, cu))
|
| 30 |
+
in_names = [i.name for i in mlmodel.input_description._fd_spec]
|
| 31 |
+
state = mlmodel.make_state()
|
| 32 |
+
|
| 33 |
+
def rel(c, t):
|
| 34 |
+
t = t.float()
|
| 35 |
+
c = c.float()
|
| 36 |
+
return ((c - t).abs().max() / t.abs().max().clamp_min(1e-9)).item()
|
| 37 |
+
|
| 38 |
+
checks = {3: cache["warmup"], 16: cache["target"]}
|
| 39 |
+
worst = 0.0
|
| 40 |
+
appear_flips = 0
|
| 41 |
+
print(f"compute_units={cu} pkg={pkg}")
|
| 42 |
+
for f in range(1, 17):
|
| 43 |
+
fi = dw.frame_inputs(model, cache, f)
|
| 44 |
+
feed = {n: v.numpy().astype(np.float32) for n, v in zip(in_names, fi)}
|
| 45 |
+
got = mlmodel.predict(feed, state=state)
|
| 46 |
+
if f in checks:
|
| 47 |
+
ref = checks[f]
|
| 48 |
+
by_shape = {tuple(np.asarray(v).shape): torch.from_numpy(
|
| 49 |
+
np.asarray(v)).float() for v in got.values()}
|
| 50 |
+
t_low = ref["pred_masks"]
|
| 51 |
+
t_osl = ref["object_score_logits"]
|
| 52 |
+
t_iou = ref["ious"]
|
| 53 |
+
c_low = by_shape[tuple(t_low.shape)]
|
| 54 |
+
c_osl = by_shape[tuple(t_osl.shape)]
|
| 55 |
+
c_iou = by_shape[tuple(t_iou.shape)]
|
| 56 |
+
r_low, r_osl, r_iou = rel(c_low, t_low), rel(c_osl, t_osl), rel(c_iou, t_iou)
|
| 57 |
+
sign = ((c_low > 0) == (t_low > 0)).float().mean().item()
|
| 58 |
+
flip = not torch.equal((c_osl > 0), (t_osl > 0))
|
| 59 |
+
appear_flips += int(flip)
|
| 60 |
+
worst = max(worst, r_low, r_osl, r_iou)
|
| 61 |
+
tag = "warmup" if f == 3 else "steady"
|
| 62 |
+
print(f"frame {f:>2} ({tag}): low_rel={r_low:.2e} osl_rel={r_osl:.2e} "
|
| 63 |
+
f"iou_rel={r_iou:.2e} sign_agree={sign:.4f} appear_flip={flip}")
|
| 64 |
+
thr = 5e-2 if "fp16" in pkg.lower() or cu == "CPU_AND_GPU" else 1e-3
|
| 65 |
+
print(f"worst rel: {worst:.3e} appear_flips: {appear_flips} thr: {thr:.0e}")
|
| 66 |
+
print("COREML RUNTIME:", "PASS" if worst < thr and appear_flips == 0 else "CHECK")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
main()
|
tracker/triton_stub/triton/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def jit(fn=None, **kw):
|
| 2 |
+
if fn is None:
|
| 3 |
+
return lambda f: f
|
| 4 |
+
return fn
|
| 5 |
+
def cdiv(a, b):
|
| 6 |
+
return (a + b - 1) // b
|
| 7 |
+
class _L:
|
| 8 |
+
def __getattr__(self, k):
|
| 9 |
+
return _L()
|
| 10 |
+
def __call__(self, *a, **k):
|
| 11 |
+
return _L()
|
| 12 |
+
language = _L()
|
tracker/triton_stub/triton/language.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class _T:
|
| 2 |
+
def __getattr__(self, k):
|
| 3 |
+
return _T()
|
| 4 |
+
def __call__(self, *a, **k):
|
| 5 |
+
return _T()
|
| 6 |
+
def __getitem__(self, k):
|
| 7 |
+
return _T()
|
| 8 |
+
|
| 9 |
+
def __getattr__(name):
|
| 10 |
+
return _T()
|
| 11 |
+
|
| 12 |
+
constexpr = _T()
|