Python API: warm-up so the first real call is fast (warmup_variants, model.warmup()), quiet logs, install extras
Browse filesfrom_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 +4 -0
- code/PYTHON.md +145 -8
- code/pyproject.toml +1 -1
- code/tt_sam3/api.py +259 -2
- code/tt_sam3/tests/test_warmup_device.py +80 -0
- code/tt_sam3/tests/test_warmup_host.py +260 -0
- code/tt_sam3/tt/ttnn_sam3_model.py +105 -3
- examples/quickstart.py +3 -1
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/
|
|
|
|
| 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 |
-
|
| 26 |
-
|
|
|
|
| 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,
|
|
|
|
| 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`).
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, "
|
| 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)
|