changh95 commited on
Commit
2b27efc
·
verified ·
1 Parent(s): d18e0a7

Python API: warm-up so the first real call is fast (warmup_variants, model.warmup()), quiet logs, install extras

Browse files

from_pretrained() now warms up the common per-request variants with dummy inputs (device and host), so the first real call is within about 10% of later calls (independently verified in fresh processes; outputs bit-identical; steady-state latency unchanged). Adds model.warmup(...) for other variants, verbose=False by default, and documents the [server,test] extras and repo-root paths. tt-model.yaml and SERVING.md are unchanged.

README.md CHANGED
@@ -37,8 +37,11 @@ Prerequisite: a Python environment with tt-metal's `ttnn`, built at tt-metal [`8
37
  ```bash
38
  hf download changh95/sam3-p150 --exclude "image/*" --local-dir sam3-p150 && cd sam3-p150
39
  pip install -e code/ # into a Python env that already has tt-metal's ttnn
 
40
  ```
41
 
 
 
42
  ```python
43
  from tt_sam3 import Sam3
44
 
@@ -63,6 +66,7 @@ result.scores # (N,) float32, high to low
63
  | Output | `presence`, `width`, `height` | Probability that the concept is in the image, and the original image size. |
64
 
65
  - `from_pretrained()` opens the chip, loads the weights and captures the device traces. The first run compiles the kernels and can take some minutes.
 
66
  - Use `with ... as model:` or call `model.close()` to release the chip.
67
  - Give a list of prompts to segment one image with many prompts. The image part runs one time, and each prompt adds about 20 ms.
68
  - The model keeps the last image on the device. When the same image comes again, the call takes about 21 ms.
 
37
  ```bash
38
  hf download changh95/sam3-p150 --exclude "image/*" --local-dir sam3-p150 && cd sam3-p150
39
  pip install -e code/ # into a Python env that already has tt-metal's ttnn
40
+ pip install -e "code/[server,test]" # also the HTTP server (fastapi, uvicorn, ...) and the tests (pytest, httpx)
41
  ```
42
 
43
+ Run the Python example from the model repo root. The demo image path `media/source_1.png` is relative to the repo root. You can also give an absolute path.
44
+
45
  ```python
46
  from tt_sam3 import Sam3
47
 
 
66
  | Output | `presence`, `width`, `height` | Probability that the concept is in the image, and the original image size. |
67
 
68
  - `from_pretrained()` opens the chip, loads the weights and captures the device traces. The first run compiles the kernels and can take some minutes.
69
+ - Write `model = Sam3.from_pretrained(warmup_variants={"prompts": ["car", "person"]})` to prepare the model and the text of your prompts. The first call is then as fast as later calls. A prompt that you did not list adds about 80 to 175 ms to the first call that uses it.
70
  - Use `with ... as model:` or call `model.close()` to release the chip.
71
  - Give a list of prompts to segment one image with many prompts. The image part runs one time, and each prompt adds about 20 ms.
72
  - The model keeps the last image on the device. When the same image comes again, the call takes about 21 ms.
code/PYTHON.md CHANGED
@@ -6,24 +6,44 @@ scores for each instance.
6
 
7
  ## Install
8
 
9
- Install the package into a Python environment that already has tt-metal's `ttnn` (and its `torch`):
 
10
 
11
  ```bash
12
- pip install -e code/ # from the model repo root; or: pip install code/
 
13
  ```
14
 
15
  The package does not declare `ttnn` or `torch`, so pip does not replace the tt-metal build of torch.
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  The weights `facebook/sam3` are gated (Meta SAM License). Request access on the Hub and run `hf auth login`
18
  once. The first `from_pretrained` downloads `sam3.pt` (3.45 GB) to the HF cache.
19
 
 
 
 
 
20
  ## Quickstart
21
 
22
  ```python
23
  from tt_sam3 import Sam3
24
 
25
- with Sam3.from_pretrained(device_id=0) as model:
26
- result = model("media/source_1.png", "car")
 
27
 
28
  print(result) # Sam3Result(prompt='car', num_instances=3, scores=[0.973, 0.817, 0.797], ...)
29
  result.masks # (N, H, W) bool, original image size
@@ -40,7 +60,8 @@ import numpy as np
40
  from PIL import Image
41
  from tt_sam3 import Sam3
42
 
43
- model = Sam3.from_pretrained() # device 0, weights from the HF cache, traces warmed up
 
44
 
45
  # Several prompts on one image: the image part runs once, each prompt adds about 20 ms.
46
  car, person, tree = model(Image.open("street.jpg"), ["car", "person", "tree"])
@@ -63,11 +84,12 @@ model.close() # or use "with ... as model
63
 
64
  ## API reference
65
 
66
- ### `Sam3.from_pretrained(repo_id="facebook/sam3", *, revision=None, checkpoint_path=None, device_id=0, device=None, dispatch="auto", warmup=2, num_threads=None, cache_image=True) -> Sam3`
67
 
68
  Loads the weights, opens the chip, converts and uploads the weights, then runs the warm-up. The first warm-up
69
  compiles the kernels and captures the device traces (minutes with a cold kernel cache, cached in
70
- `TT_METAL_CACHE`). When it returns, the model is ready and every call runs the captured traces.
 
71
 
72
  | argument | meaning |
73
  |---|---|
@@ -77,10 +99,23 @@ compiles the kernels and captures the device traces (minutes with a cold kernel
77
  | `device_id` | Chip to open. Use 0 when `TT_VISIBLE_DEVICES` pins one chip. |
78
  | `device` | An already opened `ttnn` device. Open it with `ttnn.open_device(**tt_sam3.device_open_kwargs())`. `close()` does not close a device that you gave. |
79
  | `dispatch` | `"auto"` (default): ETH dispatch and a 12x10 compute grid when the tt-metal tree has the Blackhole ETH-dispatch patch. This is how the published Galaxy numbers were measured. Without the patch, the model uses the default dispatch and gives a `RuntimeWarning`. The outputs are the same. `"eth"` or `"worker"` select the dispatch directly. |
80
- | `warmup` | Number of warm-up forwards (default 2). With 0, the first call compiles and captures. |
 
81
  | `num_threads` | Host torch threads. Default: `SAM3_NUM_THREADS`, else 17, capped at the CPU count. |
 
82
  | `cache_image` | Keep the device results of the last image. When the same image comes again (same pixels or same file bytes), the image part is skipped. The output bits are the same. This needs `xxhash`. |
83
 
 
 
 
 
 
 
 
 
 
 
 
84
  ### `model(image, prompts, *, threshold=0.5, max_instances=50, return_masks=True, return_mask_logits=False)`
85
 
86
  | argument | meaning |
@@ -131,6 +166,108 @@ closed at exit. One process can use a chip for only one model at a time.
131
  `from_pretrained` uses (`l1_small_size`, `trace_region_size`, and `dispatch_core_config` for ETH dispatch).
132
  - `model.info`: the checkpoint path, the revision, the dispatch, the compute grid and the host paths in use.
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  ## Notes
135
 
136
  - Same results as the HTTP server: `model(...)` runs the same device traces, host preprocessing, instance
 
6
 
7
  ## Install
8
 
9
+ Install the package into a Python environment that already has tt-metal's `ttnn` (and its `torch`). Run the commands
10
+ from the model repo root:
11
 
12
  ```bash
13
+ pip install -e code/ # the Python API only
14
+ pip install -e "code/[server,test]" # also the HTTP server (fastapi, uvicorn, ...) and the tests (pytest, httpx)
15
  ```
16
 
17
  The package does not declare `ttnn` or `torch`, so pip does not replace the tt-metal build of torch.
18
 
19
+ | you want to run | extras you need |
20
+ |---|---|
21
+ | `from tt_sam3 import Sam3` | none |
22
+ | the HTTP server (`python -m uvicorn tt_sam3.server.app:app`) | `server` |
23
+ | the host tests (no device, no weights) | `server` and `test`: `test_server_host.py` and `test_api_host.py` start the server app with fastapi's `TestClient` |
24
+ | the device tests | `test`, and a Blackhole chip |
25
+
26
+ Run only the host tests (about 1 minute; they do not open a device and do not need the weights):
27
+
28
+ ```bash
29
+ TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_sam3/tests/test_*_host.py
30
+ ```
31
+
32
  The weights `facebook/sam3` are gated (Meta SAM License). Request access on the Hub and run `hf auth login`
33
  once. The first `from_pretrained` downloads `sam3.pt` (3.45 GB) to the HF cache.
34
 
35
+ **Paths in the examples.** The examples use the demo image `media/source_1.png` with a path relative to the model repo
36
+ root. Run them from the repo root, or give an absolute path. `examples/quickstart.py` finds the demo image relative to
37
+ its own file, so you can run it from any directory.
38
+
39
  ## Quickstart
40
 
41
  ```python
42
  from tt_sam3 import Sam3
43
 
44
+ # run from the model repo root (the demo image path is relative to it)
45
+ with Sam3.from_pretrained(device_id=0, warmup_variants={"prompts": ["car"]}) as model:
46
+ result = model("media/source_1.png", "car") # the first call is as fast as later calls
47
 
48
  print(result) # Sam3Result(prompt='car', num_instances=3, scores=[0.973, 0.817, 0.797], ...)
49
  result.masks # (N, H, W) bool, original image size
 
60
  from PIL import Image
61
  from tt_sam3 import Sam3
62
 
63
+ model = Sam3.from_pretrained() # device 0, weights from the HF cache, warmed up
64
+ # "street.jpg", "a.jpg", ...: your own image files (paths relative to the current directory)
65
 
66
  # Several prompts on one image: the image part runs once, each prompt adds about 20 ms.
67
  car, person, tree = model(Image.open("street.jpg"), ["car", "person", "tree"])
 
84
 
85
  ## API reference
86
 
87
+ ### `Sam3.from_pretrained(repo_id="facebook/sam3", *, revision=None, checkpoint_path=None, device_id=0, device=None, dispatch="auto", warmup=2, warmup_variants=None, num_threads=None, cache_image=True, verbose=False) -> Sam3`
88
 
89
  Loads the weights, opens the chip, converts and uploads the weights, then runs the warm-up. The first warm-up
90
  compiles the kernels and captures the device traces (minutes with a cold kernel cache, cached in
91
+ `TT_METAL_CACHE`). Then it prepares the request variants of `warmup_variants` (see "Warm-up" below). When it
92
+ returns, the model is ready and the first real call is as fast as a later call.
93
 
94
  | argument | meaning |
95
  |---|---|
 
99
  | `device_id` | Chip to open. Use 0 when `TT_VISIBLE_DEVICES` pins one chip. |
100
  | `device` | An already opened `ttnn` device. Open it with `ttnn.open_device(**tt_sam3.device_open_kwargs())`. `close()` does not close a device that you gave. |
101
  | `dispatch` | `"auto"` (default): ETH dispatch and a 12x10 compute grid when the tt-metal tree has the Blackhole ETH-dispatch patch. This is how the published Galaxy numbers were measured. Without the patch, the model uses the default dispatch and gives a `RuntimeWarning`. The outputs are the same. `"eth"` or `"worker"` select the dispatch directly. |
102
+ | `warmup` | Number of trace warm-up forwards (default 2). With 0, there is no warm-up at all (also no `warmup_variants`): the first call compiles and captures. |
103
+ | `warmup_variants` | The request variants to prepare (see "Warm-up"). `None` (default): `DEFAULT_WARMUP_VARIANTS`. A dict replaces the defaults of the keys that it names. `False`: only the trace warm-up. |
104
  | `num_threads` | Host torch threads. Default: `SAM3_NUM_THREADS`, else 17, capped at the CPU count. |
105
+ | `verbose` | `False` (default): a quiet console (see "Console output"). `True`: tt-metal's INFO log and the `tt_sam3` steps. |
106
  | `cache_image` | Keep the device results of the last image. When the same image comes again (same pixels or same file bytes), the image part is skipped. The output bits are the same. This needs `xxhash`. |
107
 
108
+ ### `model.warmup(*, prompt_counts=(), image_sizes=(), max_instances=(), prompts=()) -> dict`
109
+
110
+ Prepares more request variants after `from_pretrained` (same keys as `warmup_variants`). It is idempotent: a variant
111
+ that is already prepared is skipped. It returns `{variant: seconds}` for the variants that ran.
112
+ `model.warmed_variants` and `model.info["warmup"]` show what is prepared and the total warm-up time.
113
+
114
+ ```python
115
+ model.warmup(prompts=["bicycle", "traffic light"]) # about 75 ms for each new prompt
116
+ model.warmup(image_sizes=[(720, 1280)]) # about 0.1 s
117
+ ```
118
+
119
  ### `model(image, prompts, *, threshold=0.5, max_instances=50, return_masks=True, return_mask_logits=False)`
120
 
121
  | argument | meaning |
 
166
  `from_pretrained` uses (`l1_small_size`, `trace_region_size`, and `dispatch_core_config` for ETH dispatch).
167
  - `model.info`: the checkpoint path, the revision, the dispatch, the compute grid and the host paths in use.
168
 
169
+ ## Warm-up
170
+
171
+ After `from_pretrained` returns, the first real call of a prepared variant is as fast as a later call (within about
172
+ 2 %, see the table). The warm-up uses real calls on generated photo-like images of the real sizes and dummy noun
173
+ phrases. These calls go through the full path: the device traces, the host text-encoder threads, PNG and JPEG decode,
174
+ the input kernels, the kept-instance readback, the mask upsample and its buffers. The warm-up keeps no result. The only
175
+ exception is the text features of the prompts that you give in `prompts`, which go into the normal text cache. A call
176
+ computes and caches the same values. The image cache is empty after the warm-up. The outputs do not change: the
177
+ digests of all 20 x 9 measured calls were bit-identical before and after the change.
178
+
179
+ ### Variants
180
+
181
+ | key | default | what it prepares |
182
+ |---|---|---|
183
+ | `prompt_counts` | `(1, 2, 3, 4)` | Calls with 1..k prompts (the server's `SAM3_MAX_PROMPTS` default is 4). |
184
+ | `image_sizes` | `((1080, 1920), (1008, 1008))` | `(height, width)` inputs. 1008x1008 uses its own input kernel (no resample). |
185
+ | `max_instances` | `(1, 2, 4, 8, 16, 32, 64, 200)` | The device readback bucket for the number of kept instances (powers of two up to 64; more than 64 = full mask readback). |
186
+ | `prompts` | `()` | Your noun phrases. Their text features are computed now. |
187
+
188
+ Cost of the defaults: about 3.2 s more in `from_pretrained` (15.4..16.3 s -> 18.7..19.5 s with a warm kernel cache). No more device
189
+ memory, because all traces and readback buffers exist after the trace warm-up. Each prompt in `prompts` adds about
190
+ 75 ms.
191
+
192
+ ### What the defaults give, and what needs your prompts
193
+
194
+ - The defaults prepare the host first-use costs (decoders, the first use of the text encoder) and the request
195
+ shapes (1..4 prompts, the two image sizes, all kept-instance buckets). Alone, they make call 1 faster for calls
196
+ with several prompts (2 prompts: 375 -> 238 ms, 4 prompts: 414 -> 363 ms), but not for one prompt (210 -> 207 ms).
197
+ - For a fixed prompt set, call 1 still runs the text encoder for each of your prompts (+70..+175 ms with the
198
+ defaults). Give your prompts in `warmup_variants={"prompts": [...]}` to remove this: call 1 / steady goes from
199
+ 1.5..2.0x to 0.96..1.02x.
200
+ - The text encoder has `SAM3_TEXT_WORKERS` (default 4) threads. A new prompt goes to the first idle thread, so one
201
+ new prompt per call always uses the same thread, and 4 new prompts use 4 threads in parallel. The warm-up runs its
202
+ text encodes on its own threads and stops them at the end, so the text threads of your calls are the same as
203
+ without the warm-up. The time of a call with new prompts does not change.
204
+
205
+ ### What the warm-up cannot prepare
206
+
207
+ - **A new prompt.** The prompts are open vocabulary. The host text encoder runs once for each prompt that the model
208
+ has not seen yet, and the result goes into the text cache (256 entries). This costs about 75 to 150 ms on the call
209
+ that uses the prompt first (measured: +65..+150 ms for one new prompt, about +225 ms for 4 new prompts, which encode
210
+ in parallel). A later call with a new prompt pays the same cost, so this is not a first-call cost. To remove it from
211
+ the first call, give your prompts in `warmup_variants={"prompts": [...]}` or call `model.warmup(prompts=[...])`.
212
+ - **Other image sizes.** Any size works without a first-use cost above about 1 ms (only the resample tables of
213
+ the new size are computed). In the measurement, the JPEG set has 9 different sizes and none of them was slower on
214
+ first use.
215
+
216
+ ### Measured
217
+
218
+ Chip 11 of a Galaxy Blackhole host (12x10 grid, ETH dispatch, shared host), one fresh process per row,
219
+ `perf/warmup/first_call_bench.py`. Every call uses a NEW real image (18 photos, never the warm-up input, never the
220
+ previous image). "steady" is the median of calls 3..20. The image set is 1920x1080 PNG unless stated (`sq`:
221
+ 1008x1008 PNG, `jpg`: JPEG files of 9 sizes, 333x500 to 1600x900). Columns: from_pretrained, then call
222
+ 1 / 2 / 10 / steady in ms.
223
+
224
+ | variant | before | after, defaults | after, prompts listed |
225
+ |---|---|---|---|
226
+ | 1 prompt (`"car"`) | 15.9 s, 210 / 132 / 128 / 130 | 18.8 s, 207 / 128 / 128 / 129 | 19.6 s, 127 / 128 / 128 / 128 |
227
+ | 2 prompts | 15.7 s, 375 / 157 / 165 / 152 | 19.1 s, 238 / 149 / 149 / 149 | 19.5 s, 147 / 154 / 149 / 149 |
228
+ | 4 prompts | 16.2 s, 414 / 197 / 196 / 188 | 19.2 s, 363 / 197 / 198 / 188 | 19.7 s, 187 / 197 / 193 / 188 |
229
+ | 1 prompt, numpy input | 15.6 s, 154 / 82 / 84 / 81 | 19.4 s, 165 / 83 / 83 / 82 | 20.2 s, 81 / 82 / 84 / 81 |
230
+ | 1 prompt, `return_masks=False, return_mask_logits=True` | 16.0 s, 191 / 129 / 129 / 128 | 18.9 s, 208 / 129 / 128 / 128 | 19.8 s, 128 / 128 / 127 / 128 |
231
+ | 1 prompt, `threshold=0.05, max_instances=200` | 21.4 s, 198 / 131 / 129 / 133 | 19.0 s, 207 / 129 / 128 / 133 | 19.9 s, 128 / 131 / 126 / 134 |
232
+ | 1 prompt, `sq` | 16.1 s, 177 / 106 / 107 / 108 | 19.1 s, 193 / 106 / 106 / 106 | 20.2 s, 105 / 105 / 106 / 106 |
233
+ | 1 prompt, `jpg` | 16.0 s, 157 / 85 / 82 / 83 | 19.2 s, 166 / 84 / 83 / 84 | 19.9 s, 86 / 84 / 83 / 84 |
234
+ | a new prompt on every call | 15.9 s, 217 / 228 / 219 / 209 | 18.8 s, 208 / 233 / 223 / 222 | - |
235
+
236
+ "Prompts listed" = `warmup_variants={"prompts": ["car", "person", "tree", "road"]}` (the other keys at their
237
+ defaults). The first-call cost that remains with the defaults is the text encoder for prompts that are not listed
238
+ (see above). The output digests of every call were bit-identical to "before". The JSON of every run is in
239
+ `perf/warmup/results/` (`before`, `after`, `afterp`).
240
+
241
+ Steady state when every call has NEW prompts (the host text encoder is most of the call), the same images and
242
+ prompts, old (54d1f2a, no variant warm-up) and new interleaved in fresh processes
243
+ (`perf/warmup/results/ab_54d1f2a/`):
244
+
245
+ | workload | 54d1f2a steady (ms) | now steady (ms) |
246
+ |---|---|---|
247
+ | 1 new prompt per call, numpy input, 12 pairs | 175.4 (mean) | 176.7 (mean); difference per pair -16.5..+13.6, median +1.5 |
248
+ | same, `warmup_variants=False`, 6 rounds | - | 172.9 (mean) |
249
+ | 1 new prompt per call, JPEG files, 3 pairs | 181.8 | 185.4 |
250
+ | 4 new prompts per call, 2 pairs | 354.7 | 360.9 |
251
+ | 1 fixed prompt, numpy input (the published path), 4 pairs | 81.3 | 81.1 |
252
+
253
+ The differences are inside the run-to-run noise of this shared host (about +-10 ms per run). An earlier version of the
254
+ warm-up used every text thread, and a single new prompt then moved between 4 threads: 171.8 vs 160.7 ms, slower in
255
+ 8 of 8 pairs (`perf/warmup/results/ab_4df6bea/`). The text pool and the warm-up's own text threads (see above) remove
256
+ this.
257
+
258
+ ## Console output
259
+
260
+ `from_pretrained(verbose=False)` (the default) keeps the console quiet:
261
+
262
+ - It sets `TT_LOGGER_LEVEL=error` (tt-metal's C++ log) and `LOGURU_LEVEL=WARNING` (ttnn's Python log). It does this
263
+ only when you have not set them and only before `ttnn` is imported (they are read once, at import).
264
+ - It hides two known harmless start-up warnings: sam3's CUDA autocast decorators on a CPU-only torch, and numba's TBB
265
+ version check.
266
+ - Errors and all other warnings (also the `RuntimeWarning`s of `tt_sam3`) are shown.
267
+
268
+ With `verbose=True`, nothing is changed and the `tt_sam3` logger prints its steps (load, device open, warm-up) to
269
+ stderr. To see tt-metal's log while `verbose=False`, set `TT_LOGGER_LEVEL=info` before you start Python.
270
+
271
  ## Notes
272
 
273
  - Same results as the HTTP server: `model(...)` runs the same device traces, host preprocessing, instance
code/pyproject.toml CHANGED
@@ -40,7 +40,7 @@ dependencies = [
40
 
41
  [project.optional-dependencies]
42
  server = ["fastapi", "uvicorn", "pydantic>=2", "pybase64>=1.4", "httptools>=0.6"]
43
- test = ["pytest"]
44
 
45
  [tool.setuptools.packages.find]
46
  include = ["tt_sam3*"]
 
40
 
41
  [project.optional-dependencies]
42
  server = ["fastapi", "uvicorn", "pydantic>=2", "pybase64>=1.4", "httptools>=0.6"]
43
+ test = ["pytest", "httpx"] # httpx: fastapi TestClient of the host tests
44
 
45
  [tool.setuptools.packages.find]
46
  include = ["tt_sam3*"]
code/tt_sam3/api.py CHANGED
@@ -16,12 +16,15 @@ host pre- and post-processing and the same instance selection, so the outputs ar
16
  from __future__ import annotations
17
 
18
  import atexit
 
19
  import functools
20
  import gc
21
  import io
22
  import logging
23
  import os
 
24
  import threading
 
25
  import warnings
26
  import weakref
27
  from concurrent.futures import ThreadPoolExecutor
@@ -44,6 +47,19 @@ WEIGHTS_FILENAME = "sam3.pt"
44
  NUM_QUERIES = 200
45
  MASK_RES = 288
46
  WARMUP_PROMPT = "object"
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  _GRID_OVERRIDE_ENV = "TT_METAL_CORE_GRID_OVERRIDE_TODEPRECATE"
48
  _GRID_12x10 = "11,9" # logical end coordinate (inclusive) of a 12x10 compute grid
49
 
@@ -295,6 +311,101 @@ def _warmup_input() -> torch.Tensor:
295
  return torch.cat([x, y, torch.sin(x * 3.14) * torch.cos(y * 3.14)], dim=1).contiguous()
296
 
297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  _PIN_PATCHED = [False]
299
 
300
 
@@ -348,6 +459,9 @@ class Sam3:
348
  self._xxhash = xxhash
349
  self.cache_image = bool(cache_image and xxhash is not None and self._rgb_input)
350
  self._last_decoded: List[Any] = [None, None]
 
 
 
351
  self._atexit = functools.partial(_close_at_exit, weakref.ref(self))
352
  atexit.register(self._atexit)
353
 
@@ -363,8 +477,10 @@ class Sam3:
363
  device=None,
364
  dispatch: str = "auto",
365
  warmup: int = 2,
 
366
  num_threads: Optional[int] = None,
367
  cache_image: bool = True,
 
368
  ) -> "Sam3":
369
  """Load the weights, open the chip, build the device traces and warm them up (the server's startup).
370
 
@@ -379,11 +495,34 @@ class Sam3:
379
  dispatch: ``"auto"`` (ETH dispatch + 12x10 grid when the tt-metal tree has the ETH-dispatch patch, as
380
  the published numbers; else the default dispatch with a warning), ``"eth"`` or ``"worker"``.
381
  warmup: warm-up forwards (the first one compiles the kernels and captures the traces; minutes when the
382
- kernel cache is cold). 0 makes the first call do it.
 
 
 
 
 
383
  num_threads: host torch threads (default ``SAM3_NUM_THREADS`` or 17, capped at the CPU count).
384
  cache_image: reuse the device results of the image part when the same image comes again (one entry;
385
  same output bits). Needs the optional ``xxhash`` package.
 
 
386
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  from tt_sam3.server import maskpost
388
  from tt_sam3.tt.fused_config import FusedConfig
389
  from tt_sam3.tt.ttnn_sam3_model import TtSam3ImagePipeline, _patch_pin_memory, build_sam3_model, resolve_bpe_path
@@ -443,7 +582,6 @@ class Sam3:
443
  "fast_mask": fast_mask,
444
  "torch_threads": torch.get_num_threads(),
445
  }
446
- log.info("SAM3 ready: %s", info)
447
  return cls(pipeline, device, owns_device=owns, info=info, cache_image=cache_image)
448
 
449
  # ---------------------------------------------------------------- inference
@@ -500,6 +638,125 @@ class Sam3:
500
  out.append(self(loaded, prompt_list[0] if single else prompt_list, **kwargs))
501
  return out
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  # ---------------------------------------------------------------- internals
504
  def _load_cached(self, image) -> "_Loaded":
505
  if isinstance(image, _Loaded):
 
16
  from __future__ import annotations
17
 
18
  import atexit
19
+ import contextlib
20
  import functools
21
  import gc
22
  import io
23
  import logging
24
  import os
25
+ import sys
26
  import threading
27
+ import time
28
  import warnings
29
  import weakref
30
  from concurrent.futures import ThreadPoolExecutor
 
47
  NUM_QUERIES = 200
48
  MASK_RES = 288
49
  WARMUP_PROMPT = "object"
50
+ #: Dummy prompts of the variant warm-up (text-encoder cache misses on the host text workers; never a cached answer to
51
+ #: a user prompt: a user prompt that equals one of them gets the same text features it would get anyway).
52
+ _WARMUP_WORDS = ("region", "thing", "item", "shape", "surface", "part", "area", "piece", "material", "pattern")
53
+ #: Kept-instance readback buckets of the device select path (powers of two up to 64; more than 64 = full readback).
54
+ INSTANCE_BUCKETS = (1, 2, 4, 8, 16, 32, 64, 200)
55
+ #: What ``from_pretrained(warmup_variants=None)`` prepares (see ``Sam3.warmup``). About 3 s on top of the trace
56
+ #: warm-up; no extra device memory (every trace already exists after the base warm-up).
57
+ DEFAULT_WARMUP_VARIANTS: Dict[str, Tuple] = {
58
+ "prompt_counts": (1, 2, 3, 4), # 1 .. 4 prompts per call (the server's SAM3_MAX_PROMPTS default)
59
+ "image_sizes": ((1080, 1920), (1008, 1008)), # (H, W): the resampling and the identity input kernels
60
+ "max_instances": INSTANCE_BUCKETS, # every kept-instance readback bucket
61
+ "prompts": (), # your own noun phrases: their text features are computed now
62
+ }
63
  _GRID_OVERRIDE_ENV = "TT_METAL_CORE_GRID_OVERRIDE_TODEPRECATE"
64
  _GRID_12x10 = "11,9" # logical end coordinate (inclusive) of a 12x10 compute grid
65
 
 
311
  return torch.cat([x, y, torch.sin(x * 3.14) * torch.cos(y * 3.14)], dim=1).contiguous()
312
 
313
 
314
+ def _dummy_rgb(height: int, width: int, seed: int) -> np.ndarray:
315
+ """A photo-like (H, W, 3) uint8 warm-up image: smooth gradients, blobs and noise (real sizes, real host work:
316
+ decode, resample, mask upsample). Generated, never a cached output."""
317
+ rng = np.random.default_rng(seed)
318
+ yy, xx = np.meshgrid(np.linspace(0, 1, height, dtype=np.float32), np.linspace(0, 1, width, dtype=np.float32), indexing="ij")
319
+ img = np.stack([xx, yy, 0.5 + 0.5 * np.sin(6.28 * (xx + yy))], axis=-1) * 160.0
320
+ for _ in range(6): # a few bright ellipses ("objects")
321
+ cy, cx, ry, rx = rng.uniform(0.1, 0.9), rng.uniform(0.1, 0.9), rng.uniform(0.05, 0.25), rng.uniform(0.05, 0.25)
322
+ m = ((yy - cy) / ry) ** 2 + ((xx - cx) / rx) ** 2 < 1.0
323
+ img[m] = rng.uniform(0, 255, size=3).astype(np.float32)
324
+ img += rng.normal(0, 8, size=(height, width, 1)).astype(np.float32)
325
+ return np.clip(img, 0, 255).astype(np.uint8)
326
+
327
+
328
+ def _encode(rgb: np.ndarray, fmt: str) -> bytes:
329
+ buf = io.BytesIO()
330
+ Image.fromarray(rgb).save(buf, format=fmt, **({"quality": 90} if fmt == "JPEG" else {"compress_level": 1}))
331
+ return buf.getvalue()
332
+
333
+
334
+ def _as_tuple(value, pair: bool = False) -> Tuple:
335
+ """One value or a sequence -> tuple; with ``pair``, one ``(h, w)`` pair is one value."""
336
+ if value is None:
337
+ return ()
338
+ if isinstance(value, (str, int)):
339
+ return (value,)
340
+ value = tuple(value)
341
+ if pair and len(value) == 2 and all(isinstance(v, int) for v in value):
342
+ return (value,)
343
+ return value
344
+
345
+
346
+ def normalize_warmup_variants(spec) -> Dict[str, Tuple]:
347
+ """``warmup_variants`` -> a full spec. ``None`` = :data:`DEFAULT_WARMUP_VARIANTS`; a dict replaces the defaults of the
348
+ keys it names (an empty value turns that key off); ``False`` / ``{}``-with-all-keys-empty = trace warm-up only."""
349
+ if spec is None or spec is True:
350
+ spec = {}
351
+ base = DEFAULT_WARMUP_VARIANTS
352
+ elif spec is False:
353
+ spec, base = {}, {k: () for k in DEFAULT_WARMUP_VARIANTS}
354
+ else:
355
+ base = DEFAULT_WARMUP_VARIANTS
356
+ if not isinstance(spec, dict):
357
+ raise TypeError("warmup_variants must be None, False or a dict with keys " + ", ".join(DEFAULT_WARMUP_VARIANTS))
358
+ unknown = set(spec) - set(DEFAULT_WARMUP_VARIANTS)
359
+ if unknown:
360
+ raise ValueError(f"unknown warmup_variants keys {sorted(unknown)}; known: {list(DEFAULT_WARMUP_VARIANTS)}")
361
+ out = {k: _as_tuple(spec[k], pair=k == "image_sizes") if k in spec else tuple(v) for k, v in base.items()}
362
+ for k in out["prompt_counts"]:
363
+ if not isinstance(k, int) or not 1 <= k <= 64:
364
+ raise ValueError(f"prompt_counts: ints in 1..64, got {k!r}")
365
+ sizes = []
366
+ for hw in out["image_sizes"]:
367
+ if (not isinstance(hw, (tuple, list)) or len(hw) != 2 or not all(isinstance(v, int) and 1 <= v <= 8192 for v in hw)):
368
+ raise ValueError(f"image_sizes: (height, width) int pairs in 1..8192, got {hw!r}")
369
+ sizes.append((int(hw[0]), int(hw[1])))
370
+ out["image_sizes"] = tuple(sizes)
371
+ for b in out["max_instances"]:
372
+ if not isinstance(b, int) or not 1 <= b <= NUM_QUERIES:
373
+ raise ValueError(f"max_instances: ints in 1..{NUM_QUERIES}, got {b!r}")
374
+ out["prompts"] = tuple(_check_prompts(list(out["prompts"]))[0]) if out["prompts"] else ()
375
+ return out
376
+
377
+
378
+ def _quiet_start(verbose: bool) -> None:
379
+ """Console noise policy of ``from_pretrained``. Quiet (default): tt-metal's C++ log and ttnn's loguru log show
380
+ errors / warnings only (``TT_LOGGER_LEVEL=error``, ``LOGURU_LEVEL=WARNING``), set only when the user has not set
381
+ them and only before tt-metal / loguru are loaded (they read them once). Verbose: the ``tt_sam3`` logger prints
382
+ its INFO steps (load, device open, warm-up) to stderr when it has no handler yet."""
383
+ if verbose:
384
+ if not log.handlers and not logging.getLogger().handlers:
385
+ h = logging.StreamHandler()
386
+ h.setFormatter(logging.Formatter("%(asctime)s tt_sam3 %(levelname)s %(message)s"))
387
+ log.addHandler(h)
388
+ log.setLevel(logging.INFO)
389
+ return
390
+ if "ttnn" not in sys.modules:
391
+ os.environ.setdefault("TT_LOGGER_LEVEL", "error")
392
+ if "loguru" not in sys.modules:
393
+ os.environ.setdefault("LOGURU_LEVEL", "WARNING")
394
+
395
+
396
+ @contextlib.contextmanager
397
+ def _quiet_warnings(verbose: bool):
398
+ """Hide two known harmless start-up warnings (sam3's CUDA autocast decorators on a CPU torch; numba's TBB version
399
+ probe, which then uses its OpenMP layer). Everything else, our RuntimeWarnings included, is shown."""
400
+ if verbose:
401
+ yield
402
+ return
403
+ with warnings.catch_warnings():
404
+ warnings.filterwarnings("ignore", message="CUDA is not available or torch_xla is imported", category=UserWarning)
405
+ warnings.filterwarnings("ignore", message="The TBB threading layer requires TBB version")
406
+ yield
407
+
408
+
409
  _PIN_PATCHED = [False]
410
 
411
 
 
459
  self._xxhash = xxhash
460
  self.cache_image = bool(cache_image and xxhash is not None and self._rgb_input)
461
  self._last_decoded: List[Any] = [None, None]
462
+ self._warmed: set = set() # variant atoms prepared by warmup()
463
+ self._warm_seed = 0
464
+ self.info.setdefault("warmup", {"variants": {k: [] for k in DEFAULT_WARMUP_VARIANTS}, "seconds": 0.0})
465
  self._atexit = functools.partial(_close_at_exit, weakref.ref(self))
466
  atexit.register(self._atexit)
467
 
 
477
  device=None,
478
  dispatch: str = "auto",
479
  warmup: int = 2,
480
+ warmup_variants: Optional[Union[Dict[str, Any], bool]] = None,
481
  num_threads: Optional[int] = None,
482
  cache_image: bool = True,
483
+ verbose: bool = False,
484
  ) -> "Sam3":
485
  """Load the weights, open the chip, build the device traces and warm them up (the server's startup).
486
 
 
495
  dispatch: ``"auto"`` (ETH dispatch + 12x10 grid when the tt-metal tree has the ETH-dispatch patch, as
496
  the published numbers; else the default dispatch with a warning), ``"eth"`` or ``"worker"``.
497
  warmup: warm-up forwards (the first one compiles the kernels and captures the traces; minutes when the
498
+ kernel cache is cold). 0 skips all warm-up (also ``warmup_variants``): the first call does it.
499
+ warmup_variants: the request variants to prepare after the trace warm-up, so that the first real call of
500
+ each is as fast as a later one (see :meth:`warmup`). ``None`` = :data:`DEFAULT_WARMUP_VARIANTS`
501
+ (1..4 prompts, 1920x1080 and 1008x1008 inputs, every kept-instance bucket; about 3 s). A dict
502
+ replaces the defaults of the keys it names, e.g. ``{"prompts": ["car", "person"]}`` also computes
503
+ the text features of your prompts now. ``False``: only the trace warm-up.
504
  num_threads: host torch threads (default ``SAM3_NUM_THREADS`` or 17, capped at the CPU count).
505
  cache_image: reuse the device results of the image part when the same image comes again (one entry;
506
  same output bits). Needs the optional ``xxhash`` package.
507
+ verbose: False (default) keeps the console quiet: tt-metal / ttnn print errors and warnings only (see
508
+ ``PYTHON.md``, "Console output"). True prints tt-metal's INFO log and the ``tt_sam3`` steps.
509
  """
510
+ _quiet_start(verbose)
511
+ variants = normalize_warmup_variants(warmup_variants) # check the spec before the slow part
512
+ with _quiet_warnings(verbose):
513
+ model = cls._build(repo_id, revision, checkpoint_path, device_id, device, dispatch, warmup, num_threads,
514
+ cache_image)
515
+ if warmup > 0:
516
+ try:
517
+ model.warmup(**variants)
518
+ except BaseException:
519
+ model.close()
520
+ raise
521
+ log.info("SAM3 ready: %s", model.info)
522
+ return model
523
+
524
+ @classmethod
525
+ def _build(cls, repo_id, revision, checkpoint_path, device_id, device, dispatch, warmup, num_threads, cache_image):
526
  from tt_sam3.server import maskpost
527
  from tt_sam3.tt.fused_config import FusedConfig
528
  from tt_sam3.tt.ttnn_sam3_model import TtSam3ImagePipeline, _patch_pin_memory, build_sam3_model, resolve_bpe_path
 
582
  "fast_mask": fast_mask,
583
  "torch_threads": torch.get_num_threads(),
584
  }
 
585
  return cls(pipeline, device, owns_device=owns, info=info, cache_image=cache_image)
586
 
587
  # ---------------------------------------------------------------- inference
 
638
  out.append(self(loaded, prompt_list[0] if single else prompt_list, **kwargs))
639
  return out
640
 
641
+ # ---------------------------------------------------------------- warm-up
642
+ def warmup(
643
+ self,
644
+ *,
645
+ prompt_counts: Union[int, Sequence[int]] = (),
646
+ image_sizes: Union[Tuple[int, int], Sequence[Tuple[int, int]]] = (),
647
+ max_instances: Union[int, Sequence[int]] = (),
648
+ prompts: Union[str, Sequence[str]] = (),
649
+ ) -> Dict[str, float]:
650
+ """Prepare request variants now, so that their first real call is as fast as a later one. Idempotent: a
651
+ variant that is already prepared is skipped. Called by :meth:`from_pretrained` with ``warmup_variants``.
652
+
653
+ Every variant runs real calls on generated photo-like images of the real sizes and real (dummy) prompts, through
654
+ the same path as ``model(...)``: the device traces, the host text-encoder worker threads, PNG / JPEG decode,
655
+ the threaded input kernels, the kept-instance readback, the mask upsample and its buffers. Nothing of these
656
+ calls is kept except the text features of ``prompts`` (the model's normal text cache, the same values a call
657
+ computes); the image cache is cleared afterwards. The outputs of later calls do not change.
658
+
659
+ Args:
660
+ prompt_counts: prompts per call (``model(image, [p1, .., pk])``), e.g. ``(1, 2, 3, 4)``.
661
+ image_sizes: ``(height, width)`` input sizes, e.g. ``[(1080, 1920)]``. Any other size works without a
662
+ first-use cost above about 1 ms (only its resample tables are new); 1008x1008 uses its own kernel.
663
+ max_instances: kept-instance counts to prepare: each value prepares the readback bucket of that many kept
664
+ instances (1, 2, 4, .. 64 and > 64).
665
+ prompts: your noun phrases. Their text features (the host text encoder, about 75 ms per new prompt) are
666
+ computed now, so the first call with them does not pay it. A prompt that is not listed pays it once,
667
+ on the call that first uses it (every new prompt does, also later).
668
+
669
+ Returns:
670
+ ``{variant: seconds}`` of the variants that ran now.
671
+ """
672
+ req = normalize_warmup_variants({"prompt_counts": prompt_counts, "image_sizes": image_sizes,
673
+ "max_instances": max_instances, "prompts": prompts})
674
+ if not any(req.values()):
675
+ return {}
676
+ todo = [("host", None)]
677
+ todo += [("prompt_counts", k) for k in req["prompt_counts"]]
678
+ todo += [("image_sizes", hw) for hw in req["image_sizes"]]
679
+ todo += [("max_instances", NUM_QUERIES if b > 64 else 1 << (b - 1).bit_length()) for b in req["max_instances"]] # bucket
680
+ todo += [("prompts", p) for p in req["prompts"]]
681
+ todo = [a for a in dict.fromkeys(todo) if a not in self._warmed]
682
+ done: Dict[str, float] = {}
683
+ if not todo:
684
+ return done
685
+ if self._closed:
686
+ raise RuntimeError("Sam3 model is closed")
687
+ t_all = time.perf_counter()
688
+ scratch = getattr(self.pipeline, "scratch_text_pool", None)
689
+ with self._lock, (scratch() if scratch is not None else contextlib.nullcontext()):
690
+ try:
691
+ for atom in todo:
692
+ t0 = time.perf_counter()
693
+ self._warm_one(*atom)
694
+ self._warmed.add(atom)
695
+ if atom[0] != "host":
696
+ self.info["warmup"]["variants"][atom[0]].append(list(atom[1]) if isinstance(atom[1], tuple) else atom[1])
697
+ done[f"{atom[0]}={atom[1]}"] = round(time.perf_counter() - t0, 3)
698
+ log.info("warm-up %s=%s: %.2f s", atom[0], atom[1], done[f"{atom[0]}={atom[1]}"])
699
+ finally:
700
+ self._invalidate_cache() # the last warm-up image is not kept as "the previous image"
701
+ gc.collect()
702
+ self.info["warmup"]["seconds"] = round(self.info["warmup"]["seconds"] + time.perf_counter() - t_all, 2)
703
+ return done
704
+
705
+ @property
706
+ def warmed_variants(self) -> Dict[str, list]:
707
+ """The variants :meth:`warmup` has prepared so far (``{"prompt_counts": [1, 2], ...}``)."""
708
+ return {k: list(v) for k, v in self.info["warmup"]["variants"].items()}
709
+
710
+ def _dummy_prompts(self, k: int) -> List[str]:
711
+ """k prompts that are text-encoder cache misses now (the miss path: host worker threads, as a new user prompt)."""
712
+ out = []
713
+ while len(out) < k:
714
+ self._warm_seed += 1
715
+ n = self._warm_seed
716
+ out.append(f"{_WARMUP_WORDS[n % len(_WARMUP_WORDS)]} {n}")
717
+ return out
718
+
719
+ def _warm_one(self, kind: str, value) -> None:
720
+ if kind == "host":
721
+ # host first-use costs that every request variant shares: the PNG (libdeflate fast path and PIL) and JPEG
722
+ # decoders, the process-wide first use of the host text encoder (on the warm-up's own text threads, see
723
+ # scratch_text_pool: the real calls start their text threads new)
724
+ rgb = _dummy_rgb(270, 480, 1)
725
+ for fmt in ("PNG", "JPEG"):
726
+ _decode_bytes(_encode(rgb, fmt))
727
+ encode = getattr(self.pipeline, "_cached_forward_text", None)
728
+ if encode is not None:
729
+ ps = self._dummy_prompts(max(1, int(os.environ.get("SAM3_TEXT_WORKERS", "4"))))
730
+ self.pipeline.prefetch_text(ps) # all workers at once: one encode on each worker thread
731
+ for p in ps:
732
+ encode([p], device="cpu")
733
+ return
734
+ if kind == "prompt_counts": # a new image as encoded PNG bytes (decode + cache key), k new prompts
735
+ self._warm_seed += 1
736
+ png = _encode(_dummy_rgb(1080, 1920, 100 + self._warm_seed), "PNG")
737
+ self(png, self._dummy_prompts(value))
738
+ return
739
+ if kind == "image_sizes":
740
+ h, w = value
741
+ self._warm_seed += 1
742
+ self(_dummy_rgb(h, w, 200 + self._warm_seed), self._dummy_prompts(1)[0])
743
+ return
744
+ if kind == "max_instances":
745
+ # threshold 0 keeps every query (score > 0), so max_instances = n kept instances: the readback bucket of n
746
+ # (n > 64: the full mask readback); one image, the image trace runs once (image cache)
747
+ if not hasattr(self, "_warm_inst_img"):
748
+ self._warm_seed += 1
749
+ self._warm_inst_img = _dummy_rgb(1080, 1920, 300 + self._warm_seed)
750
+ self(self._warm_inst_img, WARMUP_PROMPT, threshold=0.0, max_instances=int(value), return_masks=value <= 8)
751
+ return
752
+ if kind == "prompts": # the text features a call with this prompt computes (and caches) on its first use
753
+ encode = getattr(self.pipeline, "_cached_forward_text", None)
754
+ if encode is not None:
755
+ self.pipeline.prefetch_text([value])
756
+ encode([value], device="cpu")
757
+ return
758
+ raise ValueError(kind)
759
+
760
  # ---------------------------------------------------------------- internals
761
  def _load_cached(self, image) -> "_Loaded":
762
  if isinstance(image, _Loaded):
code/tt_sam3/tests/test_warmup_device.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Device test of the warm-up: after ``from_pretrained`` the FIRST real call is already fast, and the warm-up does not
3
+ change any output bit.
4
+
5
+ source tools/chipenv.sh <chip> <model>/.venv
6
+ timeout -s INT 1800 python -m pytest -q -s code/tt_sam3/tests/test_warmup_device.py
7
+
8
+ 1. ``from_pretrained(warmup_variants={"prompts": [...]})`` in this process (fresh model), then real calls on NEW images
9
+ (two real photos, alternated, so the one-entry image cache never hits) with 1, 2 and 4 prompts: the first call of each
10
+ variant must be within ``WARM_TOL`` (default 1.10) of the median of its later calls (+ 3 ms for timer noise).
11
+ 2. A second model with ``warmup_variants=False`` (trace warm-up only) gives the same output bits for the same calls.
12
+ """
13
+ import hashlib
14
+ import os
15
+ import statistics
16
+ import time
17
+
18
+ import numpy as np
19
+ import pytest
20
+
21
+ pytestmark = pytest.mark.skipif(os.environ.get("TT_VISIBLE_DEVICES", "") == "none", reason="needs a device")
22
+
23
+ MODEL_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
24
+ IMAGES = [os.path.join(MODEL_DIR, "media", f) for f in ("source_1.png", "target_1.png")]
25
+ VARIANTS = {"k1": ["car"], "k2": ["car", "tree"], "k4": ["car", "person", "tree", "road"]}
26
+ PROMPTS = sorted({p for v in VARIANTS.values() for p in v})
27
+ TOL = float(os.environ.get("WARM_TOL", "1.10"))
28
+ CALLS = 8
29
+
30
+
31
+ def _digest(res):
32
+ h = hashlib.sha1()
33
+ for r in res:
34
+ for a in (r.scores, r.boxes, np.packbits(r.masks)):
35
+ h.update(np.ascontiguousarray(a).tobytes())
36
+ h.update(np.float64(r.presence).tobytes())
37
+ return h.hexdigest()
38
+
39
+
40
+ def _series(model, prompts):
41
+ raws = [open(p, "rb").read() for p in IMAGES]
42
+ ms, digests = [], []
43
+ for i in range(CALLS):
44
+ t0 = time.perf_counter()
45
+ res = model(raws[i % 2], prompts) # a different image than the previous call: the whole graph runs
46
+ ms.append((time.perf_counter() - t0) * 1e3)
47
+ digests.append(_digest(res))
48
+ return ms, digests
49
+
50
+
51
+ def test_first_call_fast_and_outputs_unchanged():
52
+ from tt_sam3 import Sam3
53
+
54
+ t0 = time.perf_counter()
55
+ model = Sam3.from_pretrained(device_id=0, warmup_variants={"prompts": PROMPTS})
56
+ t_fp = time.perf_counter() - t0
57
+ info = model.info["warmup"]
58
+ print(f"\nfrom_pretrained {t_fp:.1f} s (variant warm-up {info['seconds']} s): {info['variants']}", flush=True)
59
+ assert info["variants"]["prompt_counts"] == [1, 2, 3, 4] and info["variants"]["prompts"] == PROMPTS
60
+ warm = {}
61
+ try:
62
+ for name, prompts in VARIANTS.items():
63
+ ms, dg = _series(model, prompts)
64
+ med = statistics.median(ms[2:])
65
+ print(f"{name}: first {ms[0]:.1f} ms, second {ms[1]:.1f} ms, steady median {med:.1f} ms "
66
+ f"(first / steady = {ms[0] / med:.3f})", flush=True)
67
+ warm[name] = dg
68
+ assert ms[0] <= med * TOL + 3.0, (name, ms)
69
+ assert model.warmup(prompt_counts=(1, 2), prompts=PROMPTS) == {} # idempotent: nothing runs again
70
+ finally:
71
+ model.close()
72
+
73
+ cold = Sam3.from_pretrained(device_id=0, warmup_variants=False)
74
+ try:
75
+ assert cold.info["warmup"]["variants"]["prompt_counts"] == []
76
+ for name, prompts in VARIANTS.items():
77
+ _, dg = _series(cold, prompts)
78
+ assert dg == warm[name], f"{name}: the variant warm-up changed outputs"
79
+ finally:
80
+ cold.close()
code/tt_sam3/tests/test_warmup_host.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Host tests (no device, no weights) of the warm-up API: ``from_pretrained(warmup_variants=..., verbose=...)``,
3
+ ``model.warmup(**variant)`` (spec checks, idempotence, what it runs, no effect on outputs) and the quiet console
4
+ policy.
5
+
6
+ TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_sam3/tests/test_warmup_host.py
7
+ """
8
+ import inspect
9
+ import os
10
+ import subprocess
11
+ import sys
12
+
13
+ import numpy as np
14
+ import pytest
15
+ import torch
16
+
17
+ from tt_sam3 import api
18
+
19
+ MODEL_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
20
+
21
+
22
+ class _Runner:
23
+ def __init__(self):
24
+ self.stats = {}
25
+ self.img_key = None
26
+
27
+
28
+ class _Pipe:
29
+ """Deterministic fake of the device pipeline that records what the warm-up asks for."""
30
+
31
+ def __init__(self):
32
+ self._full = _Runner()
33
+ self.calls = [] # (shape, prompts, threshold, max_instances, cache hit)
34
+ self.text = [] # encoded prompts (cache misses)
35
+ self.text_cache = {}
36
+ self.prefetched = []
37
+ self.released = 0
38
+
39
+ def prefetch_text(self, prompts):
40
+ self.prefetched.append(list(prompts))
41
+
42
+ def _cached_forward_text(self, captions, device="cpu"):
43
+ key = tuple(captions)
44
+ if key not in self.text_cache:
45
+ self.text.append(captions[0])
46
+ self.text_cache[key] = {"language_features": torch.zeros(32, 1, 256)}
47
+ return self.text_cache[key]
48
+
49
+ def release(self):
50
+ self.released += 1
51
+
52
+ def forward_rgb_selected_multi(self, rgb, prompts, threshold, max_instances, image_key=None):
53
+ hit = image_key is not None and image_key == self._full.img_key
54
+ self._full.img_key = image_key
55
+ self.calls.append((rgb.shape, list(prompts), threshold, max_instances, hit))
56
+ for p in prompts:
57
+ self._cached_forward_text([p])
58
+ m = float(np.asarray(rgb, dtype=np.float64).mean()) / 255.0
59
+ out = []
60
+ for k, _p in enumerate(prompts):
61
+ n = 200
62
+ scores = torch.linspace(0.9, 0.01, n) * (0.5 + m)
63
+ keep = torch.nonzero(scores > threshold).flatten()[:max_instances]
64
+ g = torch.Generator().manual_seed(k)
65
+ logits = torch.randn(n, api.MASK_RES, api.MASK_RES, generator=g)
66
+ boxes = torch.rand(n, 4, generator=g) * 0.5 + 0.25
67
+ out.append((scores[keep], boxes[keep], logits[keep], 0.9))
68
+ return out
69
+
70
+
71
+ def _model():
72
+ pytest.importorskip("xxhash")
73
+ pipe = _Pipe()
74
+ return api.Sam3(pipe, device=None, owns_device=False, info={"rgb_input": True, "fast_mask": True}), pipe
75
+
76
+
77
+ # ------------------------------------------------------------------------------------------------
78
+ # spec
79
+ # ------------------------------------------------------------------------------------------------
80
+ def test_from_pretrained_signature():
81
+ sig = inspect.signature(api.Sam3.from_pretrained)
82
+ assert sig.parameters["warmup_variants"].default is None
83
+ assert sig.parameters["verbose"].default is False
84
+ assert sig.parameters["warmup"].default == 2
85
+ assert "warmup_variants" in api.Sam3.from_pretrained.__doc__ and "verbose" in api.Sam3.from_pretrained.__doc__
86
+ assert callable(api.Sam3.warmup) and api.Sam3.warmup.__doc__
87
+ assert set(inspect.signature(api.Sam3.warmup).parameters) == {"self", *api.DEFAULT_WARMUP_VARIANTS}
88
+
89
+
90
+ def test_spec_defaults_and_overrides():
91
+ d = api.normalize_warmup_variants(None)
92
+ assert d["prompt_counts"] == (1, 2, 3, 4)
93
+ assert d["image_sizes"] == ((1080, 1920), (1008, 1008))
94
+ assert d["max_instances"] == api.INSTANCE_BUCKETS and d["prompts"] == ()
95
+ o = api.normalize_warmup_variants({"prompts": ["car", " person "], "image_sizes": (720, 1280)})
96
+ assert o["prompts"] == ("car", "person") and o["image_sizes"] == ((720, 1280),)
97
+ assert o["prompt_counts"] == d["prompt_counts"] # keys not named keep their defaults
98
+ assert api.normalize_warmup_variants({"prompt_counts": 2})["prompt_counts"] == (2,)
99
+ off = api.normalize_warmup_variants(False)
100
+ assert all(v == () for v in off.values())
101
+ assert api.normalize_warmup_variants({"max_instances": ()})["max_instances"] == ()
102
+
103
+
104
+ @pytest.mark.parametrize("bad", [{"foo": 1}, {"prompt_counts": 0}, {"prompt_counts": [1, "2"]}, {"image_sizes": [(10,)]},
105
+ {"image_sizes": [(0, 10)]}, {"max_instances": 201}, {"prompts": [""]}, "all"])
106
+ def test_spec_rejects(bad):
107
+ with pytest.raises((ValueError, TypeError)):
108
+ api.normalize_warmup_variants(bad)
109
+
110
+
111
+ # ------------------------------------------------------------------------------------------------
112
+ # warmup(): what it runs, idempotence, no effect on outputs
113
+ # ------------------------------------------------------------------------------------------------
114
+ def test_warmup_runs_each_variant_once():
115
+ model, pipe = _model()
116
+ done = model.warmup(prompt_counts=(1, 3), image_sizes=[(1080, 1920), (1008, 1008)], max_instances=(1, 3, 64, 100, 200),
117
+ prompts=["car", "dog"])
118
+ assert "host=None" in done and len(done) == 1 + 2 + 2 + 4 + 2 # 3 -> bucket 4; 100 and 200 -> one bucket
119
+ shapes = [c[0] for c in pipe.calls]
120
+ assert (1080, 1920, 3) in shapes and (1008, 1008, 3) in shapes # real sizes, real host work
121
+ assert sorted(len(c[1]) for c in pipe.calls if c[2] == 0.5)[:4] == [1, 1, 1, 3]
122
+ assert sorted(c[3] for c in pipe.calls if c[2] == 0.0) == [1, 4, 64, 200] # threshold 0: one call per bucket
123
+ assert "car" in pipe.text and "dog" in pipe.text
124
+ assert len(pipe.prefetched[0]) == int(os.environ.get("SAM3_TEXT_WORKERS", "4")) # every text worker thread
125
+ assert pipe._full.img_key is None and model._last_decoded == [None, None] # nothing kept as "previous image"
126
+ w = model.warmed_variants
127
+ assert w["prompt_counts"] == [1, 3] and w["max_instances"] == [1, 4, 64, 200] and w["prompts"] == ["car", "dog"]
128
+ assert w["image_sizes"] == [[1080, 1920], [1008, 1008]]
129
+ n = len(pipe.calls)
130
+ assert model.warmup(prompt_counts=3, image_sizes=(1008, 1008), max_instances=3, prompts="car") == {} # idempotent (3: bucket 4)
131
+ assert len(pipe.calls) == n
132
+ more = model.warmup(prompt_counts=2)
133
+ assert list(more) == ["prompt_counts=2"] and len(pipe.calls) == n + 1
134
+
135
+
136
+ def test_warmup_prompts_do_not_collide_with_user_prompts():
137
+ model, pipe = _model()
138
+ model.warmup(prompt_counts=(1, 2, 3, 4))
139
+ dummies = {p for c in pipe.calls for p in c[1]}
140
+ assert len(dummies) == 10 and not dummies & {"car", "person", "tree", "dog", "object"}
141
+
142
+
143
+ def test_warmup_does_not_change_outputs():
144
+ rng = np.random.default_rng(5)
145
+ img = rng.integers(0, 256, size=(300, 400, 3), dtype=np.uint8)
146
+ cold, _ = _model()
147
+ ref = cold(img, ["car", "tree"], threshold=0.3)
148
+ warm, _ = _model()
149
+ warm.warmup(**api.normalize_warmup_variants(None))
150
+ got = warm(img, ["car", "tree"], threshold=0.3)
151
+ for a, b in zip(ref, got):
152
+ assert np.array_equal(a.masks, b.masks) and np.array_equal(a.scores, b.scores) and np.array_equal(a.boxes, b.boxes)
153
+
154
+
155
+ def test_warmup_after_close_raises():
156
+ model, _ = _model()
157
+ model.close()
158
+ with pytest.raises(RuntimeError):
159
+ model.warmup(prompt_counts=1)
160
+
161
+
162
+ # ------------------------------------------------------------------------------------------------
163
+ # quiet console
164
+ # ------------------------------------------------------------------------------------------------
165
+ def _run(code, env_extra=None):
166
+ env = {k: v for k, v in os.environ.items() if k not in ("TT_LOGGER_LEVEL", "LOGURU_LEVEL")}
167
+ env.update(TT_VISIBLE_DEVICES="none", **(env_extra or {}))
168
+ out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, env=env, timeout=300)
169
+ assert out.returncode == 0, out.stderr[-2000:]
170
+ return out.stdout.strip()
171
+
172
+
173
+ def test_quiet_start_sets_log_levels_only_when_unset():
174
+ code = ("import os; from tt_sam3 import api; api._quiet_start(False); "
175
+ "print(os.environ.get('TT_LOGGER_LEVEL'), os.environ.get('LOGURU_LEVEL'))")
176
+ assert _run(code) == "error WARNING"
177
+ assert _run(code, {"TT_LOGGER_LEVEL": "debug", "LOGURU_LEVEL": "DEBUG"}) == "debug DEBUG" # the user's choice wins
178
+ verbose = ("import os, logging; from tt_sam3 import api; api._quiet_start(True); "
179
+ "print(os.environ.get('TT_LOGGER_LEVEL'), api.log.level == logging.INFO, len(api.log.handlers))")
180
+ assert _run(verbose) == "None True 1"
181
+
182
+
183
+ def test_quiet_warnings_hide_only_known_noise():
184
+ import warnings
185
+
186
+ with warnings.catch_warnings(record=True) as rec:
187
+ warnings.simplefilter("always")
188
+ with api._quiet_warnings(False):
189
+ warnings.warn("CUDA is not available or torch_xla is imported. Disabling autocast.", UserWarning)
190
+ warnings.warn("The TBB threading layer requires TBB version 2021 update 6 or later")
191
+ warnings.warn("something real", RuntimeWarning)
192
+ with api._quiet_warnings(True):
193
+ warnings.warn("CUDA is not available or torch_xla is imported. Disabling autocast.", UserWarning)
194
+ assert [str(w.message)[:14] for w in rec] == ["something real", "CUDA is not av"]
195
+
196
+
197
+ # ------------------------------------------------------------------------------------------------
198
+ # docs
199
+ # ------------------------------------------------------------------------------------------------
200
+ def test_python_md_documents_warmup_and_install_extras():
201
+ text = open(os.path.join(MODEL_DIR, "code", "PYTHON.md"), encoding="utf-8").read()
202
+ for needle in ("warmup_variants", "model.warmup(", "prompt_counts", "image_sizes", "max_instances", "prompts",
203
+ 'code/[server,test]', "test_*_host.py", "verbose", "repo root"):
204
+ assert needle in text, needle
205
+
206
+
207
+ def test_text_pool_keeps_single_prompt_jobs_on_slot0():
208
+ """The text-encoder pool sends a job to the lowest idle slot: after the warm-up has used every slot, a stream of
209
+ one new prompt per call still runs on the same (slot 0) thread; k concurrent jobs use k different slots."""
210
+ import threading
211
+ import time
212
+
213
+ from tt_sam3.tt.ttnn_sam3_model import _TextPool
214
+
215
+ pool = _TextPool(4)
216
+ try:
217
+ def name(sleep=0.0):
218
+ time.sleep(sleep)
219
+ return threading.current_thread().name
220
+
221
+ burst = [pool.submit(name, 0.2) for _ in range(4)] # the warm-up: one job on each slot at once
222
+ assert sorted(f.result() for f in burst) == [f"sam3-text{i}" for i in range(4)]
223
+ seq = [pool.submit(name).result() for _ in range(20)] # one new prompt per call
224
+ assert set(seq) == {"sam3-text0"}
225
+ two = [pool.submit(name, 0.1) for _ in range(2)]
226
+ assert [f.result() for f in two] == ["sam3-text0", "sam3-text1"]
227
+ six = [pool.submit(name, 0.1) for _ in range(6)] # more jobs than slots: the first free slots take the rest
228
+ names = [f.result() for f in six]
229
+ assert names[:4] == [f"sam3-text{i}" for i in range(4)] and set(names[4:]) <= set(names[:4])
230
+
231
+ def boom():
232
+ raise ValueError("x")
233
+
234
+ with pytest.raises(ValueError):
235
+ pool.submit(boom).result()
236
+ assert pool.submit(name).result() == "sam3-text0"
237
+ finally:
238
+ pool.shutdown()
239
+
240
+
241
+ def test_scratch_text_pool_leaves_no_warm_text_threads():
242
+ """The warm-up's text encodes run on their own threads; afterwards the real pool is as before (none yet: the
243
+ first real prompt starts a new thread, as without the warm-up)."""
244
+ import threading
245
+ import types
246
+
247
+ from tt_sam3.tt.ttnn_sam3_model import TtSam3ImagePipeline
248
+
249
+ seen = []
250
+ fake = types.SimpleNamespace(_text_dev=None, _text_cache={}, _text_cache_max=256,
251
+ _orig_forward_text=lambda *a, **k: seen.append(threading.current_thread()))
252
+ prefetch = TtSam3ImagePipeline.prefetch_text.__get__(fake)
253
+ with TtSam3ImagePipeline.scratch_text_pool(fake):
254
+ assert prefetch(["a", "b"]) == 2
255
+ tmp = fake._text_pool
256
+ [f.result() for f in fake._text_futures.values()]
257
+ assert fake._text_pool is None and all(not t.is_alive() for t in seen) and len(seen) == 2
258
+ assert prefetch(["c"]) == 1 and fake._text_pool is not tmp
259
+ fake._text_futures[(("c",), False, False)].result()
260
+ fake._text_pool.shutdown()
code/tt_sam3/tt/ttnn_sam3_model.py CHANGED
@@ -836,12 +836,12 @@ class TtSam3ImagePipeline:
836
  if os.environ.get("SAM3_TEXT_PREFETCH", "1") != "1" or getattr(self, "_text_dev", None) is not None:
837
  return 0
838
  import threading
839
- from concurrent.futures import ThreadPoolExecutor
840
 
841
- if getattr(self, "_text_pool", None) is None:
842
- self._text_pool = ThreadPoolExecutor(max_workers=int(os.environ.get("SAM3_TEXT_WORKERS", "4")), thread_name_prefix="sam3-text")
843
  self._text_futures = {}
844
  self._text_lock = threading.Lock()
 
 
845
  nthreads = torch.get_num_threads()
846
 
847
  def job(p):
@@ -861,6 +861,24 @@ class TtSam3ImagePipeline:
861
  n += 1
862
  return n
863
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
  def _cached_forward_text(self, captions, input_boxes=None, additional_text=None, device="cuda"):
865
  cache_key = (tuple(captions), input_boxes is not None, additional_text is not None)
866
  if cache_key in self._text_cache:
@@ -1037,3 +1055,87 @@ def preprocess_image(image: torch.Tensor, target_size: int = 1008) -> torch.Tens
1037
  if image.shape[-2:] != (target_size, target_size):
1038
  image = F.interpolate(image, size=(target_size, target_size), mode="bilinear", align_corners=False)
1039
  return (image - 0.5) / 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  if os.environ.get("SAM3_TEXT_PREFETCH", "1") != "1" or getattr(self, "_text_dev", None) is not None:
837
  return 0
838
  import threading
 
839
 
840
+ if getattr(self, "_text_futures", None) is None:
 
841
  self._text_futures = {}
842
  self._text_lock = threading.Lock()
843
+ if getattr(self, "_text_pool", None) is None:
844
+ self._text_pool = _TextPool(int(os.environ.get("SAM3_TEXT_WORKERS", "4")))
845
  nthreads = torch.get_num_threads()
846
 
847
  def job(p):
 
861
  n += 1
862
  return n
863
 
864
+ def scratch_text_pool(self):
865
+ """Context manager: the text encodes started inside it run on their own worker threads, which stop at its end
866
+ (the warm-up uses it). The real calls after it start their text threads new, as without the warm-up: a
867
+ measured single-prompt encode on a thread that the warm-up used was about 20 ms slower than on a new thread."""
868
+ import contextlib
869
+
870
+ @contextlib.contextmanager
871
+ def scope():
872
+ saved, self._text_pool = getattr(self, "_text_pool", None), None
873
+ try:
874
+ yield
875
+ finally:
876
+ tmp, self._text_pool = self._text_pool, saved
877
+ if tmp is not None:
878
+ tmp.shutdown(wait=True)
879
+
880
+ return scope()
881
+
882
  def _cached_forward_text(self, captions, input_boxes=None, additional_text=None, device="cuda"):
883
  cache_key = (tuple(captions), input_boxes is not None, additional_text is not None)
884
  if cache_key in self._text_cache:
 
1055
  if image.shape[-2:] != (target_size, target_size):
1056
  image = F.interpolate(image, size=(target_size, target_size), mode="bilinear", align_corners=False)
1057
  return (image - 0.5) / 0.5
1058
+
1059
+
1060
+
1061
+ class _TextPool:
1062
+ """The host text-encoder worker threads (``SAM3_TEXT_WORKERS`` slots, each thread started on its first job).
1063
+
1064
+ A new job goes to the LOWEST idle slot. When no slot is idle it waits in one shared backlog, which the first slot
1065
+ to finish takes (as a ThreadPoolExecutor does). So one new prompt per call always runs on slot 0 (one warm thread
1066
+ with its warm OpenMP pool and malloc arena), and k concurrent new prompts use slots 0..k-1. A ThreadPoolExecutor
1067
+ hands a job to ANY idle thread: once several threads exist (k > 1 prompts, or the warm-up) single-prompt jobs move
1068
+ between them and pay their colder state (measured: about +11 ms per new prompt, 172 vs 161 ms per call)."""
1069
+
1070
+ def __init__(self, n: int):
1071
+ import collections
1072
+ import threading
1073
+
1074
+ self._n = max(1, int(n))
1075
+ self._cond = threading.Condition()
1076
+ self._inbox = [None] * self._n
1077
+ self._idle = [True] * self._n
1078
+ self._threads = [None] * self._n
1079
+ self._backlog = collections.deque()
1080
+ self._shutdown = False
1081
+
1082
+ def __len__(self):
1083
+ return self._n
1084
+
1085
+ def submit(self, fn, *args):
1086
+ from concurrent.futures import Future
1087
+ import threading
1088
+
1089
+ fut = Future()
1090
+ with self._cond:
1091
+ if self._shutdown:
1092
+ raise RuntimeError("text pool is shut down")
1093
+ job = (fut, fn, args)
1094
+ slot = next((i for i in range(self._n) if self._idle[i]), None)
1095
+ if slot is None:
1096
+ self._backlog.append(job)
1097
+ else:
1098
+ self._idle[slot] = False
1099
+ self._inbox[slot] = job
1100
+ if self._threads[slot] is None:
1101
+ t = threading.Thread(target=self._worker, args=(slot,), name=f"sam3-text{slot}", daemon=True)
1102
+ self._threads[slot] = t
1103
+ t.start()
1104
+ self._cond.notify_all()
1105
+ return fut
1106
+
1107
+ def _worker(self, i: int):
1108
+ while True:
1109
+ with self._cond:
1110
+ while self._inbox[i] is None and not self._shutdown:
1111
+ self._cond.wait()
1112
+ job, self._inbox[i] = self._inbox[i], None
1113
+ if job is None:
1114
+ return
1115
+ while job is not None:
1116
+ fut, fn, args = job
1117
+ if not fut.set_running_or_notify_cancel():
1118
+ res, exc = None, None
1119
+ else:
1120
+ try:
1121
+ res, exc = fn(*args), None
1122
+ except BaseException as e: # noqa: BLE001 - handed to the caller through the future
1123
+ res, exc = None, e
1124
+ with self._cond: # the slot is idle BEFORE the result is visible: the next submit sees it idle
1125
+ job = self._backlog.popleft() if self._backlog else None
1126
+ if job is None:
1127
+ self._idle[i] = True
1128
+ if fut.running():
1129
+ if exc is None:
1130
+ fut.set_result(res)
1131
+ else:
1132
+ fut.set_exception(exc)
1133
+
1134
+ def shutdown(self, wait=True):
1135
+ with self._cond:
1136
+ self._shutdown = True
1137
+ self._cond.notify_all()
1138
+ if wait:
1139
+ for t in self._threads:
1140
+ if t is not None:
1141
+ t.join()
examples/quickstart.py CHANGED
@@ -5,6 +5,8 @@
5
  python examples/quickstart.py [image] [prompt ...]
6
 
7
  Writes examples/out/quickstart_overlay.png (masks and boxes on the image) and examples/out/quickstart.json.
 
 
8
  """
9
  import json
10
  import os
@@ -23,7 +25,7 @@ out_dir = os.path.join(HERE, "out")
23
  os.makedirs(out_dir, exist_ok=True)
24
 
25
  # --- the model card snippet -----------------------------------------------------------------------------
26
- with Sam3.from_pretrained(device_id=0) as model:
27
  results = model(image_path, prompts) # one Sam3Result per prompt
28
  t0 = time.perf_counter()
29
  model(image_path, prompts) # warm call (same image: the image part is reused)
 
5
  python examples/quickstart.py [image] [prompt ...]
6
 
7
  Writes examples/out/quickstart_overlay.png (masks and boxes on the image) and examples/out/quickstart.json.
8
+ The default image and the output directory are found relative to this file, so it runs from any directory; an image
9
+ path given on the command line is relative to the current directory.
10
  """
11
  import json
12
  import os
 
25
  os.makedirs(out_dir, exist_ok=True)
26
 
27
  # --- the model card snippet -----------------------------------------------------------------------------
28
+ with Sam3.from_pretrained(device_id=0, warmup_variants={"prompts": prompts}) as model: # warm: call 1 is fast too
29
  results = model(image_path, prompts) # one Sam3Result per prompt
30
  t0 = time.perf_counter()
31
  model(image_path, prompts) # warm call (same image: the image part is reused)