Skip to content

Commit d967fa3

Browse files
Adding SAM3 iOS Model (#106)
1 parent 162ee99 commit d967fa3

27 files changed

Lines changed: 4232 additions & 410 deletions

File tree

models/sam3/README.md

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
SAM 3 (Segment Anything Model 3) is a unified vision model from Meta for promptable image and video segmentation given text or visual prompts.[^1]
44

5+
This export targets **iOS** by restructuring the model in a BC1S layout (channels-first, `Conv2d(1x1)` projections, fp16-safe primitives, rank-4 window attention) and splitting it into three independently optimizable functions:
6+
7+
| Function | Compression | Inputs | Outputs |
8+
|----------------|--------------------------------------------|--------------------------------------|--------------------------------------------------------------|
9+
| `image_encode` | 4-bit k-means palettization (gs=32) + fp16 | `pixel_values` | `backbone_features` |
10+
| `text_encode` | 6-bit k-means palettization (gs=8) + fp16 | `input_ids` | `text_features` |
11+
| `detect` | fp16 (no weight compression) | `backbone_features`, `text_features` | `pred_masks`, `pred_boxes`, `pred_logits`, `presence_logits` |
12+
513
## Setup
614

715
If you haven't installed `uv`, install it by
@@ -23,28 +31,43 @@ hf auth login --token <YOUR_TOKEN_HERE>
2331
uv run export.py
2432
```
2533

26-
Saves to `<repo-root>/exports/<model>_<dtype>/` as a bundle directory containing `<model>_<dtype>.aimodel`, a `tokenizer/` folder, and a `metadata.json` (segmenter bundle, schema 0.2). Pass `--output-dir <path>` to override the destination.
34+
Saves to `<repo-root>/exports/<model>_lite_<image-size>_w<n-bits>_static/` as a bundle directory containing `<...>.aimodel`, a `tokenizer/` folder, and a `metadata.json` (segmenter bundle, schema 0.2). Pass `--output-dir <path>` to override the destination. If exporting the full model (vanilla HF model), saves to `<repo-root>/exports/<model>_<dtype>/`.
2735

2836
```sh
2937
uv run export.py --help
3038
```
3139

3240
**Options:**
3341

34-
| Flag | Description | Default |
35-
| -------------- | --------------------------------- | ---------------------- |
36-
| `--model` | Model variant | `facebook/sam3` |
37-
| `--output-dir` | Output directory for the bundle | `<repo-root>/exports/` |
38-
| `--dtype` | `float16`, `float32` | `float32` |
39-
| `--overwrite` | Overwrite existing bundle ||
42+
| Flag | Description | Default |
43+
|------------------------|------------------------------------------------|------------------------|
44+
| `--full` | Export plain HF `Sam3Model` (no iOS targeting) ||
45+
| `--output-dir` | Output directory for the bundle | `<repo-root>/exports/` |
46+
| `--output-name` | Custom bundle directory name | derived |
47+
| `--image-size` | Input resolution (336 lite / 1008 full) | `336` / `1008` |
48+
| `--max-text-seq-len` | (lite) Static text sequence length | `32` |
49+
| `--n-bits` | (lite) Uniform palettization bit-width override applied to BOTH encoders | asymmetric: image w4, text w6 |
50+
| `--group-size` | (lite) Uniform palettization group-size override applied to BOTH encoders | asymmetric: image gs32, text gs8 |
51+
| `--dtype` | (`--full`) Torch dtype: `float16` or `float32` | `float32` |
52+
| `--overwrite` | Overwrite existing bundle ||
53+
| `--dry-run` | Print resolved config and exit ||
4054

41-
> **Note:** Batch images and dynamic export are not currently supported.
55+
`image-size=336` is the resolution we recommend for iOS deployment.
4256

43-
**Supported models:**
57+
### Lite export (targeting iOS)
4458

45-
| Model | Parameters |
46-
| ------------- | ---------- |
47-
| facebook/sam3 | 848M |
59+
The lite export, as shown in the WWDC26 [Dive into Core AI model authoring and optimization](https://developer.apple.com/videos/play/wwdc2026/325/) video, is the default (no extra flags needed). The model is restructured for iOS in BC1S layout with palettized encoders, and split into 3 independently compressed functions (image encoder, text encoder, detector) at a 336×336 input resolution.
60+
61+
### Full export (direct from Hugging Face)
62+
63+
Pass `--full` to skip iOS targeting and palettization and emit the unmodified `transformers.Sam3Model` as a single-entrypoint asset (one `main` function returning the five raw outputs). Higher quality than the lite variant at the cost of larger model size and slower on-device inference.
64+
65+
```sh
66+
uv run models/sam3/export.py --full # float32, 1008x1008
67+
uv run models/sam3/export.py --full --dtype float16
68+
```
69+
70+
Full bundles land at `<repo-root>/exports/<model>_<dtype>/` (e.g. `exports/sam3_float32/`), so they sit next to the lite `sam3_lite_336_w4_static/` without colliding.
4871

4972
## Running
5073

@@ -54,7 +77,7 @@ uv run export.py --help
5477
import ImageSegmenter
5578

5679
// Load from a segmenter bundle directory (contains metadata.json, *.aimodel, and tokenizer/)
57-
let segmenter = try await ImageSegmenter(resourcesAt: "coreai-models/exports/sam3_float16")
80+
let segmenter = try await ImageSegmenter(resourcesAt: "coreai-models/exports/sam3_lite_336_w4_static")
5881

5982
// Text prompt (SAM3):
6083
let segments = try await segmenter.segment(image: cgImage, prompt: "cat")
@@ -66,4 +89,10 @@ let segments = try await segmenter.segment(image: cgImage, prompt: "cat")
6689
swift run -c release image-segmenter --model path/to/exported_model_folder --prompt "cat" --image path/to/image.jpg
6790
```
6891

92+
## Supported models
93+
94+
| Model | Parameters |
95+
|---------------|------------|
96+
| facebook/sam3 | 848M |
97+
6998
[^1]: [Paper](https://arxiv.org/abs/2511.16719) · [HuggingFace](https://huggingface.co/facebook/sam3)

models/sam3/export.py

Lines changed: 35 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -6,197 +6,57 @@
66
# /// script
77
# requires-python = ">=3.11"
88
# dependencies = [
9-
# "coreai-core==1.0.0b2",
10-
# "coreai-torch==0.4.1",
9+
# "coreai-models",
10+
# "transformers>=5.5.4,<5.10.1",
1111
# "tokenizers<0.23.0rc",
12+
# "huggingface-hub>=1.5.0,<2.0",
1213
# "torchvision",
13-
# "transformers>=5.5.4,<5.10.1",
1414
# ]
1515
#
1616
# [tool.uv]
1717
# index-url = "https://pypi.org/simple"
1818
# prerelease = "allow"
1919
# index-strategy = "unsafe-best-match"
20+
# # Force these past the workspace's `coreai-models` pins:
21+
# # - transformers: workspace is <5.0, SAM3 needs Sam3Model from >=5.5.4
22+
# # - huggingface-hub: workspace is <1.0, transformers 5.x requires >=1.5.0,<2.0
23+
# override-dependencies = [
24+
# "transformers>=5.5.4,<5.10.1",
25+
# "huggingface-hub>=1.5.0,<2.0",
26+
# ]
27+
#
28+
# [tool.uv.sources]
29+
# # Resolve `coreai-models` against the workspace checkout instead of PyPI so
30+
# # this script picks up local edits to `coreai_models.segmentation.*` and
31+
# # `coreai_models.models.ios.sam3.*`.
32+
# coreai-models = { path = "../../python", editable = true }
2033
# ///
21-
import argparse
22-
import json
23-
import shutil
24-
import time
25-
from pathlib import Path
26-
27-
import torch
28-
import transformers
29-
from coreai.runtime import AIModelAssetMetadata
30-
from coreai_torch import TorchConverter, get_decomp_table
31-
32-
33-
def reference_inputs(model_name: str, dtype: torch.dtype) -> dict[str, torch.Tensor]:
34-
processor = transformers.Sam3Processor.from_pretrained(model_name)
35-
text_inputs = processor.tokenizer(["dummy"], return_tensors="pt")
36-
return {
37-
"pixel_values": torch.randn(1, 3, 1008, 1008).to(dtype),
38-
"input_ids": text_inputs["input_ids"].to(torch.int32),
39-
}
40-
41-
42-
class Sam3Module(torch.nn.Module):
43-
def __init__(self, model_id: str = "facebook/sam3"):
44-
super().__init__()
45-
self._model = transformers.Sam3Model.from_pretrained(model_id)
46-
47-
def forward(self, pixel_values, input_ids):
48-
outputs = self._model(pixel_values=pixel_values, input_ids=input_ids)
49-
return (
50-
outputs.pred_masks,
51-
outputs.pred_boxes,
52-
outputs.pred_logits,
53-
outputs.presence_logits,
54-
outputs.semantic_seg,
55-
)
56-
57-
58-
def _default_output_dir() -> str:
59-
return str(Path(__file__).resolve().parents[2] / "exports")
60-
61-
62-
def _variant_name(model_name: str, dtype: torch.dtype) -> str:
63-
safe_name = Path(model_name).name
64-
dtype_name = str(dtype).split(".")[-1]
65-
return f"{safe_name}_{dtype_name}"
66-
67-
68-
def _bundle_paths(
69-
output_dir: str, model_name: str, dtype: torch.dtype
70-
) -> tuple[Path, Path]:
71-
"""Return (bundle_dir, model_path) where the .aimodel sits inside the bundle dir."""
72-
variant = _variant_name(model_name, dtype)
73-
bundle_dir = Path(output_dir) / variant
74-
return bundle_dir, bundle_dir / f"{variant}.aimodel"
75-
76-
77-
def _build_aimodel_metadata() -> AIModelAssetMetadata:
78-
# Source: https://github.com/facebookresearch/sam3
79-
metadata = AIModelAssetMetadata()
80-
metadata.author = "N. Carion et al."
81-
metadata.license = "SAM License"
82-
metadata.model_description = "SAM 3 is a unified foundation model for promptable segmentation in images and videos. It can detect, segment, and track objects using text or visual prompts such as points, boxes, and masks. This variant is explicitly for image segmentation. Source: https://github.com/facebookresearch/sam3"
83-
metadata.creation_date = int(time.time())
84-
return metadata
85-
86-
87-
def _save_asset(coreai_program, model_path: Path, overwrite: bool) -> None:
88-
bundle_dir = model_path.parent
89-
if bundle_dir.exists():
90-
if not overwrite:
91-
raise FileExistsError(
92-
f"{bundle_dir} already exists. Pass --overwrite to replace it."
93-
)
94-
shutil.rmtree(bundle_dir)
95-
bundle_dir.mkdir(parents=True, exist_ok=True)
96-
coreai_program.save_asset(model_path, _build_aimodel_metadata())
97-
98-
99-
def _write_tokenizer(dest: Path, model_id: str) -> None:
100-
print(f"[INFO] Saving tokenizer from {model_id} to {dest}...")
101-
tokenizer = transformers.AutoTokenizer.from_pretrained(model_id)
102-
tokenizer.save_pretrained(str(dest))
103-
104-
105-
def _write_bundle_metadata(bundle_dir: Path, variant: str) -> None:
106-
metadata = {
107-
"metadata_version": "0.2",
108-
"kind": "segmenter",
109-
"name": variant,
110-
"assets": {"main": f"{variant}.aimodel"},
111-
}
112-
metadata_path = bundle_dir / "metadata.json"
113-
with open(metadata_path, "w") as f:
114-
json.dump(metadata, f, indent=2)
115-
print(f"[INFO] Wrote metadata to {metadata_path}.")
116-
117-
118-
def create_sam3(
119-
output_dir: str,
120-
model_name: str,
121-
dtype: torch.dtype,
122-
overwrite: bool,
123-
):
124-
print("[INFO] Sourcing model...")
125-
model = Sam3Module(model_id=model_name)
126-
model.eval()
127-
model.to(dtype)
128-
print("[INFO] Model sourced. Running torch export with decompositions...")
129-
130-
example_inputs = reference_inputs(model_name, dtype)
131-
132-
with torch.autocast(device_type="cpu", dtype=dtype):
133-
exported = torch.export.export(
134-
model,
135-
args=(),
136-
kwargs=example_inputs,
137-
)
138-
exported = exported.run_decompositions(get_decomp_table())
139-
print("[INFO] Model exported. Converting to Core AI...")
14034

141-
converter = TorchConverter().add_exported_program(
142-
exported_program=exported,
143-
input_names=["pixel_values", "input_ids"],
144-
output_names=[
145-
"pred_masks",
146-
"pred_boxes",
147-
"pred_logits",
148-
"presence_logits",
149-
"semantic_seg",
150-
],
151-
)
152-
coreai_program = converter.to_coreai()
153-
print("[INFO] Model converted.")
154-
coreai_program.optimize()
155-
print("[INFO] Model optimized.")
35+
"""SAM3 export entry point — runs as a uv inline-script.
15636
157-
bundle_dir, model_path = _bundle_paths(output_dir, model_name, dtype)
158-
_save_asset(coreai_program, model_path, overwrite)
159-
print(f"[INFO] Successfully created and saved Core AI model to {model_path}.")
37+
The export pipeline lives in ``coreai_models.segmentation.pipeline``; this
38+
script is a thin wrapper whose only job is to give uv the right
39+
``transformers`` resolution (SAM3 needs >= 5.5.4, the workspace pins < 5.0).
40+
The PEP 723 metadata at the top of this file is what makes that possible —
41+
``uv run models/sam3/export.py`` resolves an isolated env per-script.
16042
161-
_write_tokenizer(bundle_dir / "tokenizer", model_name)
162-
_write_bundle_metadata(bundle_dir, _variant_name(model_name, dtype))
43+
Usage:
44+
uv run models/sam3/export.py [--image-size N] [--n-bits N] [--output-dir PATH] ...
45+
"""
16346

47+
import sys
16448

165-
def main():
166-
parser = argparse.ArgumentParser(
167-
description="Create and save a Core AI AIProgram for SAM3."
168-
)
169-
parser.add_argument(
170-
"--model",
171-
choices=["facebook/sam3"],
172-
default="facebook/sam3",
173-
help="Model variant to convert.",
174-
)
175-
parser.add_argument(
176-
"--output-dir",
177-
default=None,
178-
help="Output directory for the .aimodel bundle (default: <repo-root>/exports/)",
179-
)
180-
parser.add_argument(
181-
"--dtype",
182-
choices=["float16", "float32"],
183-
default="float32",
184-
help="Torch dtype to use for the model.",
185-
)
186-
parser.add_argument(
187-
"--overwrite",
188-
action="store_true",
189-
help="Overwrite an existing .aimodel asset at the output path.",
190-
)
191-
args = parser.parse_args()
19249

193-
dtype = {
194-
"float16": torch.float16,
195-
"float32": torch.float32,
196-
}[args.dtype]
50+
def main() -> None:
51+
# Lazy import so the inline-script header parses cleanly even if the
52+
# workspace package isn't on sys.path yet (uv handles that before main()).
53+
from coreai_models.segmentation.export import main as segmentation_main
19754

198-
output_dir = args.output_dir or _default_output_dir()
199-
create_sam3(output_dir, args.model, dtype, args.overwrite)
55+
# The shared CLI takes a `model` positional. This script only handles SAM3,
56+
# so inject "sam3" and forward any user-supplied flags untouched.
57+
if len(sys.argv) == 1 or sys.argv[1].startswith("-"):
58+
sys.argv.insert(1, "sam3")
59+
segmentation_main()
20060

20161

20262
if __name__ == "__main__":

python/src/coreai_models/export/metadata.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ class AIModelMetadataFields:
171171
"Source: https://huggingface.co/black-forest-labs/FLUX.2-klein-4B"
172172
),
173173
),
174+
# ---- Segmentation ----
175+
"facebook/sam3": AIModelMetadataFields(
176+
author="N. Carion et al.",
177+
license="SAM License",
178+
model_description=(
179+
"SAM 3 is a unified foundation model for promptable segmentation in "
180+
"images and videos. It can detect, segment, and track objects using "
181+
"text or visual prompts such as points, boxes, and masks. This export "
182+
"targets image segmentation, re-authored for iOS. "
183+
"Source: https://github.com/facebookresearch/sam3"
184+
),
185+
),
174186
}
175187

176188

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2026 Apple Inc.
2+
#
3+
# Use of this source code is governed by a BSD-3-clause license that can
4+
# be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
5+
6+
"""SAM3 Lite (`facebook/sam3`) for iOS export.
7+
8+
The submodules mirror the HuggingFace `Sam3Model` structure, but every
9+
component is in BC1S layout with `nn.Linear` replaced by `nn.Conv2d(1x1)`,
10+
GELU/LayerNorm/RoPE re-implemented in fp16-safe primitives, and
11+
window-attention partitioning kept at rank 4.
12+
"""
13+
14+
from coreai_models.models.ios.sam3.sam3_reauthored import Sam3Lite
15+
16+
__all__ = ["Sam3Lite"]

0 commit comments

Comments
 (0)