sdkv2 commited on
Commit
47d8ad6
·
verified ·
1 Parent(s): 0d732c8

Merge tracker + backbone, drop kino references

Browse files
Files changed (28) hide show
  1. README.md +101 -0
  2. backbone/models/sam3_vision_backbone.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  3. backbone/models/sam3_vision_backbone.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  4. backbone/models/sam3_vision_backbone.mlpackage/Manifest.json +18 -0
  5. tracker/models/dense_sam3_trackstep.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  6. tracker/models/dense_sam3_trackstep.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  7. tracker/models/dense_sam3_trackstep.mlpackage/Manifest.json +18 -0
  8. tracker/models/dense_sam3_trackstep.mlpackage/executorch_debug_handle_mapping.json +0 -0
  9. tracker/models/dense_sam3_trackstep_fp32.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  10. tracker/models/dense_sam3_trackstep_fp32.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  11. tracker/models/dense_sam3_trackstep_fp32.mlpackage/Manifest.json +18 -0
  12. tracker/models/dense_sam3_trackstep_fp32.mlpackage/executorch_debug_handle_mapping.json +0 -0
  13. tracker/patches/optimize_state.patch +22 -0
  14. tracker/scripts/ane_compile_test.py +61 -0
  15. tracker/scripts/common.py +153 -0
  16. tracker/scripts/convert_fp16.py +52 -0
  17. tracker/scripts/coreml_traj_test.py +52 -0
  18. tracker/scripts/dense_wrapper.py +329 -0
  19. tracker/scripts/eager_baseline.py +112 -0
  20. tracker/scripts/export_coreml.py +102 -0
  21. tracker/scripts/export_fp32.py +91 -0
  22. tracker/scripts/test_cross_state.py +67 -0
  23. tracker/scripts/toy_ane_test.py +17 -0
  24. tracker/scripts/toy_bisect.py +37 -0
  25. tracker/scripts/toy_minstate.py +23 -0
  26. tracker/scripts/verify_coreml_lean.py +70 -0
  27. tracker/triton_stub/triton/__init__.py +12 -0
  28. 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()