File size: 7,758 Bytes
d18e0a7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | # SPDX-License-Identifier: Apache-2.0
"""Device test of the Python API: ``Sam3(...)`` output == the server / pipeline output for the demo image (bit for
bit), and the warm ``model(...)`` time is not slower than the published model-call number.
source tools/chipenv.sh <chip> <model>/.venv
timeout -s INT 1800 python -m pytest -q -s code/tt_sam3/tests/test_api_device.py
The published model-call number (served_bench "selected", OPT_REPORT audit integration 2026-10-04, chip 7: decoded
1920x1080 demo image -> forward + selection, 1 prompt) is 82.32 ms median / 80.82 ms min. ``API_PERF_JSON=<path>``
writes the measured numbers.
"""
import base64
import json
import os
import statistics
import time
import types
import numpy as np
import pytest
import torch
pytestmark = pytest.mark.skipif(os.environ.get("TT_VISIBLE_DEVICES", "") == "none", reason="needs a device")
MODEL_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
DEMO = os.path.join(MODEL_DIR, "media", "source_1.png")
PUBLISHED_MODEL_CALL_MS = {"median": 82.32, "min": 80.82}
# published demo results (served_bench / server_smoke, rounds 4-6 and the audit integration: identical every time)
PUBLISHED = {
"car": {"scores": [0.9731, 0.8171, 0.7969], "areas": [175155, 390, 3587],
"boxes": [[545.17, 447.36, 1159.19, 854.73], [392.59, 533.37, 419.39, 549.49], [964.91, 510.56, 1077.54, 584.65]]},
"person": {"scores": [0.5936], "areas": [386]},
"tree": {"scores": [0.9174, 0.87, 0.7769, 0.5161, 0.5029], "areas": [33417, 38059, 4255, 5903, 1674]},
}
ITERS = int(os.environ.get("API_PERF_ITERS", "30"))
@pytest.fixture(scope="module")
def model():
from tt_sam3 import Sam3
t0 = time.perf_counter()
m = Sam3.from_pretrained(device_id=0)
print(f"\nfrom_pretrained: {time.perf_counter() - t0:.1f} s, info={m.info}", flush=True)
yield m
m.close()
assert m.closed
def _server_results(model, prompts):
"""The HTTP server's request handler (``_predict_core``) on the same pipeline."""
from tt_sam3.server import app as A
raw = open(DEMO, "rb").read()
saved = dict(A.STATE)
A.STATE.clear()
A.STATE.update(pipeline=model.pipeline, config=types.SimpleNamespace(max_prompts=8, max_side=4096),
rgb_input=model.info["rgb_input"])
try:
A._LAST_DECODED[:] = [None, None]
return A._predict_core(raw, prompts, 0.5, 50, "png", A._decode_bytes, "enc")
finally:
A.STATE.clear()
A.STATE.update(saved)
def test_api_equals_server_and_pipeline(model):
from PIL import Image
from tt_sam3.api import load_image
prompts = ["car", "person", "tree"]
res = model(DEMO, prompts, return_mask_logits=True)
# 1) the server's handler, same pipeline, same image
j = _server_results(model, prompts)
assert j["width"] == 1920 and j["height"] == 1080
for r, jr in zip(res, j["results"]):
assert r.prompt == jr["prompt"] and len(r) == jr["num_instances"] and round(r.presence, 4) == jr["presence"]
for i, inst in enumerate(jr["instances"]):
assert round(float(r.scores[i]), 4) == inst["score"]
assert [round(float(v), 2) for v in r.boxes[i]] == inst["box_xyxy"]
png = np.asarray(Image.open(__import__("io").BytesIO(base64.b64decode(inst["mask_png"])))) > 0
assert np.array_equal(r.masks[i], png) and int(r.masks[i].sum()) == inst["mask_area"]
# 2) the pipeline's selection tuples, unrounded: bit-identical scores / boxes / mask logits / presence
rgb = load_image(DEMO)
model._invalidate_cache()
with torch.inference_mode():
sel = model.pipeline.forward_rgb_selected_multi(rgb, prompts, 0.5, 50)
for r, (scores, boxes, logits, presence) in zip(res, sel):
assert np.array_equal(r.scores, scores.float().numpy()) and r.presence == float(presence)
assert np.array_equal(r.mask_logits, logits.float().numpy())
from tt_sam3.api import boxes_cxcywh_to_xyxy_pixels
assert np.array_equal(r.boxes, boxes_cxcywh_to_xyxy_pixels(boxes, 1920, 1080))
# 3) the published demo results
for r in res:
pub = PUBLISHED[r.prompt]
assert [round(float(s), 4) for s in r.scores] == pub["scores"], r
assert [int(m.sum()) for m in r.masks] == pub["areas"], r
if "boxes" in pub:
assert [[round(float(v), 2) for v in b] for b in r.boxes] == pub["boxes"]
# 4) every input kind and the image cache give the same bits
pil = Image.open(DEMO)
for src in (rgb, np.ascontiguousarray(rgb), pil, torch.from_numpy(np.ascontiguousarray(rgb)).permute(2, 0, 1), DEMO):
r2 = model(src, "car")
assert np.array_equal(r2.masks, res[0].masks) and np.array_equal(r2.scores, res[0].scores)
assert np.array_equal(r2.boxes, res[0].boxes)
model(rgb, "car") # the loop ended with the file (an encoded-bytes key): this call makes rgb resident
hits = model.pipeline._full.stats.get("image_cache_hits", 0)
model(rgb, "person") # same pixels: the image part is skipped
assert model.pipeline._full.stats.get("image_cache_hits", 0) == hits + 1
many = model.predict_many([DEMO, rgb], "car")
assert all(np.array_equal(m.masks, res[0].masks) for m in many)
def _times(fn, n):
t = []
for _ in range(n):
t0 = time.perf_counter()
fn()
t.append((time.perf_counter() - t0) * 1e3)
return {"median": round(statistics.median(t), 2), "min": round(min(t), 2)}
def test_api_warm_call_time(model):
from tt_sam3.api import load_image
rgb = np.ascontiguousarray(load_image(DEMO))
pipe = model.pipeline
def server_path(): # what the server's handler runs for a decoded image (no cache key, 1 prompt)
with torch.inference_mode():
pipe.forward_rgb_selected_multi(rgb, ["car"], 0.5, 50)
out = {"published_model_call_ms": PUBLISHED_MODEL_CALL_MS, "iters": ITERS, "info": {k: str(v) for k, v in model.info.items()}}
model.cache_image = False # every call runs the whole image graph (as for a new image)
for f in (server_path, lambda: model(rgb, "car", return_masks=False), lambda: model(rgb, "car")):
_times(f, 5) # warm
rounds = {"pipeline_direct": [], "api_no_masks": [], "api_masks": []}
for _ in range(3): # interleaved A/B rounds
rounds["pipeline_direct"].append(_times(server_path, ITERS // 3))
rounds["api_no_masks"].append(_times(lambda: model(rgb, "car", return_masks=False), ITERS // 3))
rounds["api_masks"].append(_times(lambda: model(rgb, "car"), ITERS // 3))
for k, v in rounds.items():
out[k] = {"median": round(statistics.median(x["median"] for x in v), 2), "min": min(x["min"] for x in v)}
out["api_path_with_decode"] = _times(lambda: model(DEMO, "car"), ITERS) # PNG read + decode every call (no cache)
model.cache_image = True
model(rgb, "car")
out["api_repeated_image_cached"] = _times(lambda: model(rgb, "car"), ITERS)
out["api_3_prompts_new_image"] = None
model.cache_image = False
out["api_3_prompts_new_image"] = _times(lambda: model(rgb, ["car", "person", "tree"]), ITERS // 2)
model.cache_image = True
print("\n" + json.dumps(out, indent=1), flush=True)
if os.environ.get("API_PERF_JSON"):
with open(os.environ["API_PERF_JSON"], "w") as f:
json.dump(out, f, indent=1)
# forward + selection through the API (no mask upsample) vs the published number and vs the direct call
tol = float(os.environ.get("API_PERF_TOL", "1.05"))
assert out["api_no_masks"]["median"] <= PUBLISHED_MODEL_CALL_MS["median"] * tol, out
assert out["api_no_masks"]["median"] <= out["pipeline_direct"]["median"] * tol, out
|