sam3-mlx 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. sam3_mlx/__init__.py +55 -0
  2. sam3_mlx/_device.py +2 -0
  3. sam3_mlx/_unsupported.py +363 -0
  4. sam3_mlx/assets/bpe_simple_vocab_16e6.txt.gz +0 -0
  5. sam3_mlx/convert.py +298 -0
  6. sam3_mlx/logger.py +67 -0
  7. sam3_mlx/mlx_runtime.py +85 -0
  8. sam3_mlx/model/__init__.py +1 -0
  9. sam3_mlx/model/act_ckpt_utils.py +36 -0
  10. sam3_mlx/model/box_ops.py +228 -0
  11. sam3_mlx/model/data_misc.py +362 -0
  12. sam3_mlx/model/decoder.py +1496 -0
  13. sam3_mlx/model/edt.py +138 -0
  14. sam3_mlx/model/encoder.py +506 -0
  15. sam3_mlx/model/geometry_encoders.py +830 -0
  16. sam3_mlx/model/grid_sample_mlx.py +198 -0
  17. sam3_mlx/model/io_utils.py +734 -0
  18. sam3_mlx/model/maskformer_segmentation.py +388 -0
  19. sam3_mlx/model/memory.py +222 -0
  20. sam3_mlx/model/model_misc.py +1162 -0
  21. sam3_mlx/model/multiplex_mask_decoder.py +512 -0
  22. sam3_mlx/model/multiplex_utils.py +459 -0
  23. sam3_mlx/model/necks.py +321 -0
  24. sam3_mlx/model/position_encoding.py +111 -0
  25. sam3_mlx/model/roi_align.py +198 -0
  26. sam3_mlx/model/sam1_task_predictor.py +441 -0
  27. sam3_mlx/model/sam3_base_predictor.py +270 -0
  28. sam3_mlx/model/sam3_image.py +777 -0
  29. sam3_mlx/model/sam3_image_processor.py +587 -0
  30. sam3_mlx/model/sam3_multiplex_base.py +4330 -0
  31. sam3_mlx/model/sam3_multiplex_detector.py +764 -0
  32. sam3_mlx/model/sam3_multiplex_detector_utils.py +641 -0
  33. sam3_mlx/model/sam3_multiplex_tracking.py +3067 -0
  34. sam3_mlx/model/sam3_multiplex_video_predictor.py +43 -0
  35. sam3_mlx/model/sam3_tracker_base.py +1012 -0
  36. sam3_mlx/model/sam3_tracker_utils.py +465 -0
  37. sam3_mlx/model/sam3_tracking_predictor.py +1461 -0
  38. sam3_mlx/model/sam3_video_base.py +565 -0
  39. sam3_mlx/model/sam3_video_inference.py +675 -0
  40. sam3_mlx/model/sam3_video_predictor.py +151 -0
  41. sam3_mlx/model/text_encoder_ve.py +324 -0
  42. sam3_mlx/model/tokenizer_ve.py +248 -0
  43. sam3_mlx/model/utils/__init__.py +43 -0
  44. sam3_mlx/model/utils/misc.py +116 -0
  45. sam3_mlx/model/utils/sam1_utils.py +94 -0
  46. sam3_mlx/model/utils/sam2_utils.py +240 -0
  47. sam3_mlx/model/video_tracking_multiplex.py +3176 -0
  48. sam3_mlx/model/video_tracking_multiplex_demo.py +1508 -0
  49. sam3_mlx/model/vitdet.py +823 -0
  50. sam3_mlx/model/vl_combiner.py +354 -0
  51. sam3_mlx/model_builder.py +2729 -0
  52. sam3_mlx/perflib/__init__.py +7 -0
  53. sam3_mlx/perflib/associate_det_trk.py +112 -0
  54. sam3_mlx/perflib/compile.py +107 -0
  55. sam3_mlx/perflib/connected_components.py +125 -0
  56. sam3_mlx/perflib/fa3.py +101 -0
  57. sam3_mlx/perflib/fused.py +19 -0
  58. sam3_mlx/perflib/iou.py +152 -0
  59. sam3_mlx/perflib/masks_ops.py +199 -0
  60. sam3_mlx/perflib/nms.py +160 -0
  61. sam3_mlx/rle.py +244 -0
  62. sam3_mlx/sam/__init__.py +51 -0
  63. sam3_mlx/sam/common.py +66 -0
  64. sam3_mlx/sam/mask_decoder.py +275 -0
  65. sam3_mlx/sam/prompt_encoder.py +159 -0
  66. sam3_mlx/sam/rope.py +165 -0
  67. sam3_mlx/sam/transformer.py +266 -0
  68. sam3_mlx/visualization_utils.py +636 -0
  69. sam3_mlx-0.1.0.dist-info/METADATA +192 -0
  70. sam3_mlx-0.1.0.dist-info/RECORD +73 -0
  71. sam3_mlx-0.1.0.dist-info/WHEEL +5 -0
  72. sam3_mlx-0.1.0.dist-info/licenses/LICENSE +61 -0
  73. sam3_mlx-0.1.0.dist-info/top_level.txt +1 -0
sam3_mlx/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = [
6
+ "Sam3MlxUnsupportedError",
7
+ "build_tracker",
8
+ "build_sam3_image_model",
9
+ "build_sam3_multiplex_video_model",
10
+ "build_sam3_multiplex_video_predictor",
11
+ "build_sam3_predictor",
12
+ "build_sam3_video_model",
13
+ "build_sam3_video_predictor",
14
+ "download_ckpt_from_hf",
15
+ ]
16
+
17
+
18
+ def __getattr__(name: str):
19
+ if name == "Sam3MlxUnsupportedError":
20
+ from sam3_mlx._unsupported import Sam3MlxUnsupportedError
21
+
22
+ return Sam3MlxUnsupportedError
23
+ if name == "build_tracker":
24
+ from sam3_mlx.model_builder import build_tracker
25
+
26
+ return build_tracker
27
+ if name == "build_sam3_image_model":
28
+ from sam3_mlx.model_builder import build_sam3_image_model
29
+
30
+ return build_sam3_image_model
31
+ if name == "build_sam3_multiplex_video_model":
32
+ from sam3_mlx.model_builder import build_sam3_multiplex_video_model
33
+
34
+ return build_sam3_multiplex_video_model
35
+ if name == "build_sam3_multiplex_video_predictor":
36
+ from sam3_mlx.model_builder import build_sam3_multiplex_video_predictor
37
+
38
+ return build_sam3_multiplex_video_predictor
39
+ if name == "build_sam3_predictor":
40
+ from sam3_mlx.model_builder import build_sam3_predictor
41
+
42
+ return build_sam3_predictor
43
+ if name == "build_sam3_video_model":
44
+ from sam3_mlx.model_builder import build_sam3_video_model
45
+
46
+ return build_sam3_video_model
47
+ if name == "build_sam3_video_predictor":
48
+ from sam3_mlx.model_builder import build_sam3_video_predictor
49
+
50
+ return build_sam3_video_predictor
51
+ if name == "download_ckpt_from_hf":
52
+ from sam3_mlx.model_builder import download_ckpt_from_hf
53
+
54
+ return download_ckpt_from_hf
55
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
sam3_mlx/_device.py ADDED
@@ -0,0 +1,2 @@
1
+ def is_mlx_runtime_device(device) -> bool:
2
+ return device in (None, "mlx")
@@ -0,0 +1,363 @@
1
+ """Canonical fail-fast contract for unsupported SAM3 MLX features."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from typing import Any, NamedTuple, NoReturn, TypeVar, cast
8
+
9
+ UnsupportedReason = str
10
+
11
+ # Single source of truth for the upstream SAM3 commit every port was audited
12
+ # against. Pass upstream_commit=... explicitly only when a file is re-ported
13
+ # from a different commit.
14
+ UPSTREAM_COMMIT = "2814fa619404a722d03e9a012e083e4f293a4e53"
15
+
16
+ REASONS = frozenset(
17
+ {
18
+ "agent-llm",
19
+ "eval-stack",
20
+ "flash-attn-3",
21
+ "image-interactivity",
22
+ "optional-dependency",
23
+ "port-gap",
24
+ "torch-autograd",
25
+ "torch-compile",
26
+ "torch-distributed",
27
+ "torchcodec",
28
+ "training-loop",
29
+ "triton-kernel",
30
+ "unsupported-device",
31
+ "video-multi-gpu",
32
+ "video-multiplex",
33
+ "video-tracker",
34
+ "xformers",
35
+ }
36
+ )
37
+
38
+ UNSUPPORTED_METADATA_ATTR = "__sam3_mlx_unsupported__"
39
+
40
+
41
+ class UnsupportedFeatureInfo(NamedTuple):
42
+ feature: str
43
+ reason: UnsupportedReason
44
+ alternative: str | None
45
+ detail: str | None
46
+ upstream_commit: str | None
47
+
48
+
49
+ class Sam3MlxUnsupportedError(NotImplementedError):
50
+ """Raised when an upstream SAM3 feature has no MLX implementation."""
51
+
52
+ def __init__(
53
+ self,
54
+ feature: str,
55
+ *,
56
+ reason: UnsupportedReason,
57
+ alternative: str | None = None,
58
+ detail: str | None = None,
59
+ upstream_commit: str | None = UPSTREAM_COMMIT,
60
+ ) -> None:
61
+ if reason not in REASONS:
62
+ known = ", ".join(sorted(REASONS))
63
+ raise ValueError(
64
+ f"Unknown unsupported reason {reason!r}; use one of: {known}."
65
+ )
66
+
67
+ self.feature = feature
68
+ self.reason = reason
69
+ self.alternative = alternative
70
+ self.detail = detail
71
+ self.upstream_commit = upstream_commit
72
+
73
+ message = f"{feature} is not supported in sam3_mlx ({reason})."
74
+ if detail:
75
+ message = f"{message} {detail}"
76
+ if alternative:
77
+ message = f"{message} Use {alternative} instead."
78
+ if upstream_commit:
79
+ message = f"{message} Upstream source commit: {upstream_commit}."
80
+ super().__init__(message)
81
+
82
+
83
+ def unsupported(
84
+ feature: str,
85
+ *,
86
+ reason: UnsupportedReason,
87
+ alternative: str | None = None,
88
+ detail: str | None = None,
89
+ upstream_commit: str | None = UPSTREAM_COMMIT,
90
+ ) -> Sam3MlxUnsupportedError:
91
+ return Sam3MlxUnsupportedError(
92
+ feature,
93
+ reason=reason,
94
+ alternative=alternative,
95
+ detail=detail,
96
+ upstream_commit=upstream_commit,
97
+ )
98
+
99
+
100
+ def raise_unsupported(
101
+ feature: str,
102
+ *,
103
+ reason: UnsupportedReason,
104
+ alternative: str | None = None,
105
+ detail: str | None = None,
106
+ upstream_commit: str | None = UPSTREAM_COMMIT,
107
+ ) -> NoReturn:
108
+ raise unsupported(
109
+ feature,
110
+ reason=reason,
111
+ alternative=alternative,
112
+ detail=detail,
113
+ upstream_commit=upstream_commit,
114
+ )
115
+
116
+
117
+ F = TypeVar("F", bound=Callable[..., Any])
118
+
119
+
120
+ def unsupported_function(
121
+ feature: str,
122
+ *,
123
+ reason: UnsupportedReason,
124
+ alternative: str | None = None,
125
+ detail: str | None = None,
126
+ upstream_commit: str | None = UPSTREAM_COMMIT,
127
+ ) -> Callable[[F], F]:
128
+ """Decorate an import-compatible function that always fails fast."""
129
+
130
+ info = UnsupportedFeatureInfo(
131
+ feature=feature,
132
+ reason=reason,
133
+ alternative=alternative,
134
+ detail=detail,
135
+ upstream_commit=upstream_commit,
136
+ )
137
+
138
+ def decorator(fn: F) -> F:
139
+ @wraps(fn)
140
+ def wrapper(*args: Any, **kwargs: Any) -> NoReturn:
141
+ raise_unsupported(
142
+ feature,
143
+ reason=reason,
144
+ alternative=alternative,
145
+ detail=detail,
146
+ upstream_commit=upstream_commit,
147
+ )
148
+
149
+ setattr(wrapper, UNSUPPORTED_METADATA_ATTR, info)
150
+ return cast(F, wrapper)
151
+
152
+ return decorator
153
+
154
+
155
+ def reject_kwargs(
156
+ feature: str,
157
+ *,
158
+ reason: UnsupportedReason,
159
+ alternative: str | None = None,
160
+ detail: str | None = None,
161
+ upstream_commit: str | None = UPSTREAM_COMMIT,
162
+ **kwargs: Any,
163
+ ) -> None:
164
+ """Reject truthy unsupported keyword flags with the canonical error type."""
165
+
166
+ for name, value in kwargs.items():
167
+ if value:
168
+ raise_unsupported(
169
+ f"{feature}({name}={value!r})",
170
+ reason=reason,
171
+ alternative=alternative,
172
+ detail=detail,
173
+ upstream_commit=upstream_commit,
174
+ )
175
+
176
+
177
+ def get_unsupported_info(obj: Any) -> UnsupportedFeatureInfo | None:
178
+ """Return the registered UnsupportedFeatureInfo for an object, if any."""
179
+ info = getattr(obj, UNSUPPORTED_METADATA_ATTR, None)
180
+ if isinstance(info, UnsupportedFeatureInfo):
181
+ return info
182
+ return None
183
+
184
+
185
+ def unsupported_features(
186
+ package: str = "sam3_mlx",
187
+ *,
188
+ on_import_error: Callable[[str, BaseException], None] | None = None,
189
+ ) -> list[UnsupportedFeatureInfo]:
190
+ """Walk `package` and collect every @unsupported_function entry.
191
+
192
+ Submodules that fail to import (e.g. optional heavy deps missing) are
193
+ skipped silently unless ``on_import_error`` is supplied.
194
+ """
195
+ import importlib
196
+ import pkgutil
197
+
198
+ seen_ids: set[int] = set()
199
+ collected: list[UnsupportedFeatureInfo] = []
200
+
201
+ try:
202
+ root = importlib.import_module(package)
203
+ except BaseException as exc:
204
+ if on_import_error is not None:
205
+ on_import_error(package, exc)
206
+ return collected
207
+
208
+ def _scan(module: Any) -> None:
209
+ for name in dir(module):
210
+ if name.startswith("__"):
211
+ continue
212
+ try:
213
+ obj = getattr(module, name)
214
+ except BaseException:
215
+ continue
216
+ info = get_unsupported_info(obj)
217
+ if info is None:
218
+ continue
219
+ ident = id(obj)
220
+ if ident in seen_ids:
221
+ continue
222
+ seen_ids.add(ident)
223
+ collected.append(info)
224
+
225
+ _scan(root)
226
+ if hasattr(root, "__path__"):
227
+ for module_info in pkgutil.walk_packages(
228
+ root.__path__, prefix=root.__name__ + "."
229
+ ):
230
+ try:
231
+ submodule = importlib.import_module(module_info.name)
232
+ except BaseException as exc:
233
+ if on_import_error is not None:
234
+ on_import_error(module_info.name, exc)
235
+ continue
236
+ _scan(submodule)
237
+
238
+ collected.sort(key=lambda info: (info.reason, info.feature))
239
+ return collected
240
+
241
+
242
+ _UNSUPPORTED_DOCS_MARKER_BEGIN = "<!-- BEGIN AUTOGENERATED:UNSUPPORTED -->"
243
+ _UNSUPPORTED_DOCS_MARKER_END = "<!-- END AUTOGENERATED:UNSUPPORTED -->"
244
+
245
+
246
+ def render_unsupported_markdown(
247
+ features: list[UnsupportedFeatureInfo] | None = None,
248
+ ) -> str:
249
+ """Render the registered unsupported features as a Markdown section.
250
+
251
+ Output is deterministic so the table can be checked into source and asserted
252
+ in CI. Pass ``features`` to override the registry walk; ``None`` uses
253
+ ``unsupported_features()``.
254
+ """
255
+ if features is None:
256
+ features = unsupported_features()
257
+
258
+ lines: list[str] = [
259
+ "## Unsupported upstream features",
260
+ "",
261
+ (
262
+ "Every entry below corresponds to an `@unsupported_function` stub. "
263
+ "Invoking one raises `sam3_mlx.Sam3MlxUnsupportedError` "
264
+ "(a subclass of `NotImplementedError`)."
265
+ ),
266
+ "",
267
+ f"Total registered features: **{len(features)}**.",
268
+ "",
269
+ "| Reason | Feature | Alternative | Upstream commit |",
270
+ "| --- | --- | --- | --- |",
271
+ ]
272
+ for info in features:
273
+ feature = info.feature.replace("|", r"\|")
274
+ alternative = (info.alternative or "").replace("|", r"\|")
275
+ commit = (info.upstream_commit or "")[:7]
276
+ lines.append(f"| `{info.reason}` | `{feature}` | {alternative} | {commit} |")
277
+ return "\n".join(lines) + "\n"
278
+
279
+
280
+ def write_unsupported_section(path: Any) -> bool:
281
+ """Update the auto-generated section in ``path`` with the latest render.
282
+
283
+ The file must contain the BEGIN/END HTML-comment markers. Returns True if
284
+ the file content changed.
285
+ """
286
+ import pathlib
287
+
288
+ p = pathlib.Path(path)
289
+ original = p.read_text()
290
+ if (
291
+ _UNSUPPORTED_DOCS_MARKER_BEGIN not in original
292
+ or _UNSUPPORTED_DOCS_MARKER_END not in original
293
+ ):
294
+ raise ValueError(
295
+ f"{p} is missing the autogenerated unsupported-features markers."
296
+ )
297
+ before, _, rest = original.partition(_UNSUPPORTED_DOCS_MARKER_BEGIN)
298
+ _, _, after = rest.partition(_UNSUPPORTED_DOCS_MARKER_END)
299
+ block = (
300
+ f"{_UNSUPPORTED_DOCS_MARKER_BEGIN}\n"
301
+ f"{render_unsupported_markdown()}"
302
+ f"{_UNSUPPORTED_DOCS_MARKER_END}"
303
+ )
304
+ updated = f"{before}{block}{after}"
305
+ if updated == original:
306
+ return False
307
+ p.write_text(updated)
308
+ return True
309
+
310
+
311
+ def _cli() -> int:
312
+ """``python -m sam3_mlx._unsupported [--write PATH] [--check PATH]``."""
313
+ import argparse
314
+ import pathlib
315
+ import sys
316
+
317
+ parser = argparse.ArgumentParser(prog="sam3_mlx._unsupported")
318
+ parser.add_argument("--write", help="Path to update in place (e.g. UPSTREAM.md).")
319
+ parser.add_argument(
320
+ "--check",
321
+ help="Path to verify; exit 1 if the autogenerated section is stale.",
322
+ )
323
+ args = parser.parse_args()
324
+
325
+ if args.write:
326
+ changed = write_unsupported_section(args.write)
327
+ sys.stdout.write(f"{'updated' if changed else 'unchanged'}: {args.write}\n")
328
+ return 0
329
+
330
+ if args.check:
331
+ p = pathlib.Path(args.check)
332
+ original = p.read_text()
333
+ if _UNSUPPORTED_DOCS_MARKER_BEGIN not in original:
334
+ sys.stderr.write(f"{p}: missing autogenerated markers.\n")
335
+ return 1
336
+ before, _, rest = original.partition(_UNSUPPORTED_DOCS_MARKER_BEGIN)
337
+ _, _, after = rest.partition(_UNSUPPORTED_DOCS_MARKER_END)
338
+ block = (
339
+ f"{_UNSUPPORTED_DOCS_MARKER_BEGIN}\n"
340
+ f"{render_unsupported_markdown()}"
341
+ f"{_UNSUPPORTED_DOCS_MARKER_END}"
342
+ )
343
+ expected = f"{before}{block}{after}"
344
+ if expected != original:
345
+ sys.stderr.write(
346
+ f"{p}: autogenerated section is stale. "
347
+ f"Run `python -m sam3_mlx._unsupported --write {p}`.\n"
348
+ )
349
+ return 1
350
+ sys.stdout.write(f"{p}: up to date.\n")
351
+ return 0
352
+
353
+ sys.stdout.write(render_unsupported_markdown())
354
+ return 0
355
+
356
+
357
+ if __name__ == "__main__": # pragma: no cover - CLI entrypoint
358
+ # Re-import the package copy so the registry walker's isinstance check
359
+ # against UnsupportedFeatureInfo sees the same class as the decorated stubs
360
+ # (running `python -m sam3_mlx._unsupported` otherwise loads two copies).
361
+ import sam3_mlx._unsupported as _pkg
362
+
363
+ raise SystemExit(_pkg._cli())