README
sam-mlx
MLX inference port of Meta's SAM 2.1. The most tested checkpoint is
facebook/sam2.1-hiera-small; the converter understands the SAM2.1 tiny,
small, base-plus, and large Hugging Face checkpoints.
The runtime package is Python 3.14 + MLX and does not install PyTorch. PyTorch is
only used through the optional torch-parity extra for checkpoint conversion and
parity fixtures.
Current Checkpoint
Expected local source checkpoint:
checkpoints/sam2.1_hiera_small.pt
Converted MLX checkpoint:
checkpoints/sam2.1_hiera_small_image_segmenter.safetensors
This converted checkpoint includes:
- Hiera image encoder
- FPN neck
- prompt encoder
- mask decoder
- object pointer projection
- memory encoder
- memory attention
The older image-encoder-only conversion may also exist locally:
checkpoints/sam2.1_hiera_small_image_encoder.safetensors
Generated checkpoints are ignored by git.
Setup
uv sync --python 3.14
For Torch parity and conversion scripts:
uv sync --python 3.14 --extra torch-parity
Reference repositories are expected locally but are not runtime dependencies:
third_party/sam2
references/mlx-vlm
Convert Weights
uv run --extra torch-parity sam-mlx-convert \
--hf-id facebook/sam2.1-hiera-small \
--output-dir checkpoints
This writes:
checkpoints/sam2.1_hiera_small_image_segmenter.safetensors
Supported Hugging Face source ids:
facebook/sam2.1-hiera-tiny
facebook/sam2.1-hiera-small
facebook/sam2.1-hiera-base-plus
facebook/sam2.1-hiera-large
Convert a local Torch checkpoint:
uv run --extra torch-parity sam-mlx-convert \
--checkpoint checkpoints/sam2.1_hiera_small.pt \
--model-id facebook/sam2.1-hiera-small \
--output checkpoints/sam2.1_hiera_small_image_segmenter.safetensors
The old script path remains as a compatibility wrapper:
uv run --extra torch-parity python scripts/convert_image_encoder_weights.py \
--checkpoint checkpoints/sam2.1_hiera_small.pt \
--model-id facebook/sam2.1-hiera-small
Parity Fixtures
Generate Torch image-embedding fixtures:
uv run --extra torch-parity python scripts/export_torch_image_embeddings.py --frames 2
uv run python scripts/compare_image_embeddings.py
Generate Torch prompted-mask fixtures:
uv run --extra torch-parity python scripts/export_torch_prompt_mask.py
uv run python scripts/compare_prompt_mask.py
Current parity results:
- Image
vision_featuresmax abs error: about1.63e-05 - Prompted low-res masks max abs error: about
4.67e-05 - Prompted IoU max abs error: about
4.77e-07
Reports are written under:
outputs/parity/
Image Segmentation
Run one prompted frame and write an overlay:
uv run python scripts/predict_image_mask.py \
--point 500 610 \
--output-video outputs/image_prompt_overlay.mp4 \
--output-mask outputs/image_prompt_mask.npy
Coordinates are in the resized 1024x1024 SAM input space.
Video Tracking
Mask-prompt feedback baseline:
uv run python scripts/propagate_video_masks.py --frames 30
SAM2 memory tracker:
uv run python scripts/track_video_memory.py --frames 289 \
--point 500 610 \
--output-video outputs/dog_memory_overlay_full_v3.mp4 \
--output-mask outputs/dog_memory_masks_full_v3.npy \
--report outputs/benchmarks/dog_memory_latency_full_v3.json
Package API, matching the official SAM2 video predictor method names:
import numpy as np
from sam_mlx import SAM2VideoPredictor
predictor = SAM2VideoPredictor(
checkpoint="checkpoints/sam2.1_hiera_small_image_segmenter.safetensors"
)
state = predictor.init_state("third_party/sam2/demo/data/gallery/01_dog.mp4")
frame_idx, obj_ids, masks = predictor.add_new_points_or_box(
state,
frame_idx=0,
obj_id=1,
points=np.array([[625.0, 429.0]], dtype=np.float32),
labels=np.array([1], dtype=np.int32),
)
for frame_idx, obj_ids, masks in predictor.propagate_in_video(state):
# masks is a NumPy float32 array shaped O,1,H,W in original video resolution.
pass
Implemented API methods:
SAM2VideoPredictor.from_pretrained(...)init_state(...)add_new_points_or_box(...)add_new_points(...)add_new_mask(...)propagate_in_video(...)clear_all_prompts_in_frame(...)reset_state(...)
The current memory tracker uses:
- first-frame point prompt
- SAM2 memory encoder
- SAM2 memory attention
- object pointers
- dynamic multimask fallback on unstable single-mask tracking outputs
- conditioning-frame memory plus SAM2-style frame-indexed temporal memory
selection - click-frame masks binarized before memory encoding, matching the official
postprocessing path
The dog-gallery parity run compares MLX against the official Torch
SAM2VideoPredictor output on all 289 frames:
- Mean mask IoU over all frames: about
0.977 - Median mask IoU on non-empty Torch frames: about
0.979 - Presence match:
289 / 289frames - Official Torch overlay:
outputs/torch_sam2_dog_overlay_full_twitter.mp4 - MLX overlay:
outputs/dog_memory_overlay_full_v3_twitter.mp4 - Comparison report:
outputs/benchmarks/dog_memory_mlx_vs_torch_full_v3.json
To regenerate the official Torch dog fixture:
uv run --extra torch-parity python scripts/run_torch_sam2_dog_video.py
To regenerate the MLX-vs-Torch dog comparison report:
uv run python scripts/compare_video_masks.py \
--reference outputs/torch_sam2_dog_masks_full.npy \
--candidate outputs/dog_memory_masks_full_v3.npy \
--output outputs/benchmarks/dog_memory_mlx_vs_torch_full_v3.json
Feature Parity Benchmarks
Generate official Torch fixtures for the video UX/state features we still need
to replicate:
uv run --extra torch-parity python scripts/run_torch_video_feature_benchmarks.py \
--scenario all \
--frames 130 \
--frames-dir outputs/torch_feature_benchmark_frames_130f \
--output-dir outputs/feature_benchmarks_130f
This writes T,O,H,W mask fixtures and per-scenario reports for:
multi_objectbox_promptnegative_clickscross_frame_correctionsbidirectional_middle
The current generated fixture summary is:
outputs/feature_benchmarks_130f/torch_feature_benchmarks_summary.json
Run MLX and compare every tracked feature against the Torch fixtures:
uv run python scripts/run_feature_regression.py --frames 130
To regenerate Torch fixtures first, use:
uv run python scripts/run_feature_regression.py --refresh-torch --frames 130
During inner-loop work, compare existing outputs without rerunning MLX:
uv run python scripts/run_feature_regression.py --skip-mlx --frames 130
Use the same comparator directly for a single scenario:
uv run python scripts/track_video_features_mlx.py \
--scenario multi_object \
--frames 130 \
--output-dir outputs/feature_benchmarks_130f
uv run python scripts/compare_video_masks.py \
--reference outputs/feature_benchmarks_130f/multi_object_torch_masks.npy \
--candidate outputs/feature_benchmarks_130f/multi_object_mlx_masks.npy \
--output outputs/feature_benchmarks_130f/multi_object_mlx_vs_torch.json
Current MLX-vs-Torch feature benchmark results on the 130-frame dog-gallery
fixture:
multi_object: mean IoU0.973, presence260 / 260box_prompt: mean IoU0.953, presence129 / 130negative_clicks: mean IoU0.972, presence130 / 130cross_frame_corrections: mean IoU0.974, presence130 / 130bidirectional_middle: mean IoU0.924, presence128 / 130
The bidirectional case is currently functionally correct but still below the
other scenarios. The remaining mismatch is a two-frame object-presence boundary
near occlusion: Torch keeps tiny masks on frames 76-77, while MLX gates those
frames as no-object.
On another gallery video, 02_cups.mp4, the same bidirectional benchmark with a
center-cup prompt at frame 120 is substantially tighter:
bidirectional_middleon cups: mean IoU0.979, presence130 / 130- Report:
outputs/cups_feature_benchmarks_130f/bidirectional_middle_mlx_vs_torch.json - Overlay:
outputs/cups_feature_benchmarks_130f/bidirectional_middle_mlx_overlay.mp4
The MLX feature runner now uses a shared video state for multiple objects. It
encodes each frame once, keeps per-object conditioning/non-conditioning memory
banks, and supports NLE-style correction replay:
- forward replay from a correction frame for positive/negative click edits
- bidirectional replay from a middle-frame correction
- preserved masks outside the edited replay range
An MLX-only inspection scenario for middle-frame editor correction is available:
uv run python scripts/track_video_features_mlx.py \
--scenario nle_bidirectional_correction \
--frames 90 \
--output-dir outputs/feature_benchmarks_130f
Feature reports and inspection overlays are written under:
outputs/feature_benchmarks_130f/
Overlay Utility
Render masks onto a video:
uv run python scripts/overlay_masks.py \
--masks outputs/dog_memory_masks_full_v3.npy \
--output outputs/dog_memory_overlay_from_masks.mp4
The overlay script accepts .npy or .npz masks shaped T,H,W or T,1,H,W.
Synthetic overlays are only for writer smoke tests and require:
uv run python scripts/overlay_masks.py --synthetic-smoke-test
Benchmarks
Image encoder:
uv run --extra torch-parity python scripts/benchmark_image_encoder.py --warmup 3 --runs 10
Prompt segmentation:
uv run python scripts/benchmark_prompt_segmenter.py --warmup 3 --runs 20
Video memory tracking:
uv run python scripts/track_video_memory.py --frames 150 \
--report outputs/benchmarks/video_memory_latency_150f.json
Official Torch-vs-MLX full-video memory benchmark with multiple positive and
negative clicks:
uv run --extra torch-parity python scripts/benchmark_video_memory_torch.py \
--model-id facebook/sam2.1-hiera-small \
--checkpoint checkpoints/sam2.1_hiera_small.pt \
--frames-dir outputs/video_memory_multiclick/small_frames_full \
--output-mask outputs/video_memory_multiclick/small_torch_masks_full.npy \
--output-video outputs/video_memory_multiclick/small_torch_overlay_full.mp4 \
--report outputs/benchmarks/video_memory_multiclick_small_torch_full.json \
--points 625 429 700 470 300 250 950 610 \
--labels 1 1 0 0
uv run python scripts/benchmark_video_memory_mlx.py \
--model-id facebook/sam2.1-hiera-small \
--weights checkpoints/sam2.1_hiera_small_image_segmenter.safetensors \
--frames-dir outputs/video_memory_multiclick/small_frames_full \
--precompute-image-features \
--feature-batch-size 4 \
--memory-dtype bfloat16 \
--memory-attention-dtype bfloat16 \
--output-mask outputs/video_memory_multiclick/small_mlx_masks_full_precompute_bf16_attn.npy \
--output-video outputs/video_memory_multiclick/small_mlx_overlay_full_precompute_bf16_attn.mp4 \
--report outputs/benchmarks/video_memory_multiclick_small_mlx_full_precompute_bf16_attn.json \
--points 625 429 700 470 300 250 950 610 \
--labels 1 1 0 0
Current indicative numbers on this machine:
- Image encoder MLX: about
81 ms/frame - Image encoder Torch/MPS: about
104 ms/frame - MLX image encoder speedup: about
1.28x - Cached prompt decode: about
4 ms - Full image + prompt: about
85 ms - Dog full-video memory tracker: about
269 ms/frameon the 289-frame run - Dog full-video multi-click memory tracker, post-prompt propagation:
- Official Torch: about
331 ms/frame - MLX on-demand with bfloat memory attention: about
351 ms/frame - MLX with batched image-feature precompute and bfloat memory attention:
about189 ms/frame
- Official Torch: about
- Dog full-video total for the same multi-click run:
- Official Torch: about
100.5 s - MLX with batched image-feature precompute and bfloat memory attention:
about94.8 s
- Official Torch: about
- Dog full-video raw propagation, excluding mask saving, overlay, and final
video-resolution output resize:- Official Torch raw: about
407 ms/frame - MLX raw with bfloat memory attention: about
287 ms/frame
- Official Torch raw: about
Benchmark reports are written under:
outputs/benchmarks/
Runtime Dependency Boundary
Default runtime should not include Torch:
uv sync --python 3.14
uv run python - <<'PY'
import importlib.util as u
print({m: bool(u.find_spec(m)) for m in ["torch", "torchvision", "hydra", "iopath", "mlx", "cv2"]})
PY
Expected:
torch=False, torchvision=False, hydra=False, iopath=False, mlx=True, cv2=True