facebookresearch / facebookresearch/sam3

SAM3 → ONNX → Triton Inference Server: full image-model deployment (text, box + cross-crop exemplars, interactive PVS)

Open
#609 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
11.7k
Forks
1.8k
PR merge metrics
No merged PRs in 30d

Description

Sharing a deployment path that I couldn't find elsewhere in the issues here: **SAM3's image model exported to ONNX and served on NVIDIA Triton Inference Server**, with the full prompting surface exposed as runtime inputs.

**https://github.com/ilanmotiei/sam3-triton** (MIT)

Several excellent ONNX/TensorRT exports already exist (#224, #159). What this adds is the **serving layer** — real ONNX graphs on Triton's ONNX backend with `config.pbtxt` and ensembles, rather than a python-backend wrapper around the PyTorch code — plus the parts of the image model that usually get dropped in an export.

### The split

```
images_u8[1,3,H,W] ─► sam3_unified_encoder ─► 7 vision tensors ┐
token_ids[1,32] ────► sam3_text ────────────► 3 text tensors ─┼─► sam3_decoder ─► sam3_postprocess ─► RLE
box / concept exemplars ─► gate ────────────► 3 tensors ───────┘
```

- The ViT backbone (~1.9 GB) runs **once per image**; every prompt is a cheap runtime input on the grounding head.
- One backbone serves **both** heads — the grounding neck and the PVS (interactive) neck — instead of shipping two ~1.9 GB encoders.
- SAM3's BPE tokenizer can't be represented in ONNX, so it stays client-side and the ensemble takes `token_ids`. Since it pads to 32, every text tensor is fixed-shape — **no dynamic axes anywhere**.

### What it covers

| | |
|---|---|
| **text prompts** | open-vocabulary, prompt is a runtime input |
| **box exemplars** | positive *and* negative, in the same image |
| **cross-crop concepts** | encode an appearance on crop A, reuse it on crop B (see below) |
| **multi-keyword** | encode once, decode per keyword server-side |
| **interactive (PVS)** | point / box / mask prompts, plus automatic mask generation |

### The cross-crop concept split

This one was the interesting part. In `sam3_decoder`, the geometry encoder lives *inside* the graph and pools the **current** image's features at the box — so a box exemplar is a pointer into the image being segmented, and a box from crop A prompts with garbage on crop B.

Splitting `forward_grounding` at the boundary where `geo_feats` is consumed turns the concept into a **portable tensor**:

```
sam3_concept_encoder : 7 vision + boxes -> concept_tokens[18,1,256]
sam3_decoder_ct : 7 vision + text + tokens -> the same head outputs
```

Feeding the encoder's output into `sam3_decoder_ct` reproduces the original decoder **bit-for-bit on the same image** (torch `max|diff| = 0`, 200/200 query agreement). The tokens are image-independent once produced, so you can bank a concept and reuse it on any raster later.

### Results

Real output on the bundled sample tile (1008×1008), reproducible with `examples.py`:

![segment](https://raw.githubusercontent.com/ilanmotiei/sam3-triton/main/docs/images/segment.jpg)

`segment(tile, "building")` → 142 instances.

![multi](https://raw.githubusercontent.com/ilanmotiei/sam3-triton/main/docs/images/multi.jpg)

`multi(tile, ["building", "road", "vegetation"])` → 142 / 13 / 65, one vision pass.

### Export gotchas that might save someone time

- **`aten::view_as_complex`** — the vitdet RoPE uses complex rotary embeddings ONNX can't represent. `sam3_rope_patch.py` swaps in an algebraic real/imag rotation.
- **`aten::_pin_memory` NYI** in the fake-tensor tracer — made identity (value-neutral host transfer).
- **`GuardOnDataDependentSymNode`** in the decoder — forcing the static-cache path avoids the assert-only guards.
- **The 0-box/0-point dummy prompt** exports to a rank-mismatched `Add` that ORT rejects; passing one fully-masked box+point keeps text-only results identical.

### The Python client

The repo also ships `sam3_triton_client`, so you're not hand-rolling gRPC tensor plumbing. It runs the BPE tokenizer, frames the image, and decodes the server's condensed RLE payload back into masks.

```bash
pip install ./client
```
```python
from sam3_triton_client import Sam3Client
c = Sam3Client("localhost:8001") # gRPC; connect_timeout= blocks until models load
```

Everything returns a `pandas.DataFrame` with **`box`** (`[x1,y1,x2,y2]` in the *original* image's pixels), **`score`**, and a full-frame `HxW` **`mask`** — so results drop straight into pandas/numpy without any coordinate bookkeeping on your side.

**Text prompt** — all instances of a concept:
```python
df = c.segment("sample_tile.jpg", "building")
```
```text
box score mask
0 [107.0, 0.0, 173.0, 44.0] 0.796 <1008x1008 px, 2277 fg>
1 [194.0, 59.0, 231.0, 90.0] 0.785 <1008x1008 px, 941 fg>
... 142 rows total
```

**Box exemplars** — steer the concept with examples in the same image. Not a spatial filter: a positive box means "things that look like this", a negative one suppresses an appearance.
```python
c.segment(tile, "building", exemplar_boxes=[POS]) # 133
c.segment(tile, "building", exemplar_boxes=[POS, NEG], exemplar_labels=[1, 0]) # 132
c.segment(tile, None, exemplar_boxes=[POS]) # pure visual query, no text
```
![exemplars](https://raw.githubusercontent.com/ilanmotiei/sam3-triton/main/docs/images/exemplar_boxes.jpg)

**Multi-keyword** — one encode, a `keyword` column on the way back:
```python
df = c.multi(tile, ["building", "road", "vegetation"]) # 142 / 13 / 65
```

**Portable concepts** — encode an appearance once, reuse it anywhere, including across processes:
```python
concept = c.encode_exemplars(cropA, boxes=[[120, 80, 210, 160]])
open("concept.npz","wb").write(concept.to_bytes()) # bank it
df = c.segment(cropB, prompt=None, concept=concept) # find it on a different crop
```

**Interactive session** — encode once, then cheap prompts; each call returns 3 candidates ranked by predicted IoU:
```python
s = c.interactive(tile)
s.add_point((504, 504)) # positive click -> best IoU 0.845
s.add_point((470, 470), positive=False) # negative refinement -> best IoU 0.903
s.add_box([470, 480, 560, 545]) # box prompt -> best IoU 0.709
s.add_mask(df.iloc[0]["mask"]) # seed from a segment() result, then refine by hand
s.undo(); s.reset() # reset keeps the cached embedding
df = s.everything(points_per_side=16) # automatic mask generation -> 47 objects
```
![interactive](https://raw.githubusercontent.com/ilanmotiei/sam3-triton/main/docs/images/interactive_prompts.jpg)

Other conveniences worth knowing:

- **Any input dtype.** `uint16`/float rasters are percentile-stretched to 8-bit (robust to hot pixels), grayscale is replicated to RGB, alpha dropped — so 12-bit satellite tiles work without pre-conversion.
- **Client-side dedup.** SAM3 grounding is set prediction with **no server-side NMS**, so at low thresholds it emits masks nested inside larger ones. `dedup=` (default 0.85) suppresses those by overlap coefficient `inter/min(area)` — plain IoU-NMS misses them because the union stays large.
- **Per-request hyperparameters.** `score_thr` / `mask_thr` / `max_det` are optional ensemble inputs, so you tune without redeploying: on the sample tile `score_thr` 0.2/0.3/0.5/0.7 → 153/142/93/17 instances.
- **Connection handling.** round-robin LB (point it at a headless Service to spread across pods), gRPC-native retries for transient transport errors only, and `connect_timeout` to block through a cold start — a full SAM3 load is ~60–90 s.

### Caveats

- **Image model only** — no video, tracking, or memory propagation.
- **fp32.** The ViT correlations overflowed fp16 in my testing.
- Fixed **1008** image input and **32**-token text; other sizes need a re-export.
- **No weights shipped** — the repo is tooling; you run the export against your own checkpoint, and SAM3 itself stays under Meta's license.

Happy to answer questions on the split or the Triton configs. Disclosure: this is my repo.

Contributor guide

Open the contributing guide

Research direction

This issue presents the external sam3-triton repository rather than naming a change in facebookresearch/sam3. Start with its examples.py, Triton config.pbtxt files, sam3_rope_patch.py, and sam3_triton_client; the payload does not define a target file, test, or acceptance criteria for work in this repository.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
ai, devops
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.