sam3-p150 / code /tt_sam3 /tests /test_server_host.py
changh95's picture
Python API (2026-10-04): pip install -e code/, from_pretrained() + model(...), Python-first quickstart
d18e0a7 verified
Raw History Blame Contribute Delete
12.1 kB
# SPDX-License-Identifier: Apache-2.0
"""Host tests (no device) for the serving layer: the image-result cache (SAM3_IMG_CACHE) key / hit / invalidation
logic in the device runner and in server/app.py, and the HTTP routes with a fake pipeline (FastAPI TestClient).
TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_sam3/tests/test_server_host.py
"""
import base64
import io
import os
import sys
import types
import numpy as np
import pytest
import torch
from PIL import Image
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # <repo>/code
from tt_sam3.server import app as A # noqa: E402
from tt_sam3.tt import ttnn_sam3_device_graph as dg # noqa: E402
xxhash = pytest.importorskip("xxhash")
# ------------------------------------------------------------------------------------------------
# device runner: _run_image key logic (fake ttnn, fake upload)
# ------------------------------------------------------------------------------------------------
def _fake_runner(monkeypatch):
log = []
fake_ttnn = types.SimpleNamespace(
execute_trace=lambda dev, tid, cq_id=0, blocking=False: log.append(("replay", tid)),
release_trace=lambda dev, tid: log.append(("release", tid)),
)
monkeypatch.setattr(dg, "ttnn", fake_ttnn)
monkeypatch.setattr(dg, "_BANDED_INPUT", True)
r = object.__new__(dg.TtSam3FullDevice)
r.device = None
r.stats = {}
r.img_key = None
r.trace_img, r.trace_id = "IMG", "PROMPT"
r.hx_bands = ["band"]
r.g = types.SimpleNamespace(_dec0=None)
r._upload_bands = lambda px: log.append(("upload", px))
return r, log
def test_run_image_hit_skips_upload_and_trace(monkeypatch):
r, log = _fake_runner(monkeypatch)
r._run_image("px1", ("enc", 1, b"a"))
assert log == [("upload", "px1"), ("replay", "IMG")]
r._run_image("px1", ("enc", 1, b"a")) # same key: nothing on the device
assert len(log) == 2 and r.stats["image_cache_hits"] == 1
r._run_image("px2", ("enc", 1, b"b")) # different image: full path, the key moves
assert log[2:] == [("upload", "px2"), ("replay", "IMG")]
r._run_image("px1", ("enc", 1, b"a")) # one entry only: the first image is no longer resident
assert log[4:] == [("upload", "px1"), ("replay", "IMG")]
assert r.stats["image_cache_hits"] == 1 and r.stats["image_replays"] == 3
def test_run_image_no_key_always_runs_and_clears(monkeypatch):
r, log = _fake_runner(monkeypatch)
r._run_image("px1", ("enc", 1, b"a"))
r._run_image("px1") # forward_image / __call__ path: no key -> runs, and the resident image is now unkeyed
assert r.img_key is None
r._run_image("px1", ("enc", 1, b"a"))
assert [e for e in log if e[0] == "replay"] == [("replay", "IMG")] * 3
assert "image_cache_hits" not in r.stats
def test_run_image_failed_upload_leaves_no_key(monkeypatch):
r, log = _fake_runner(monkeypatch)
r._run_image("px1", ("enc", 1, b"a"))
def boom(px):
raise RuntimeError("upload failed")
r._upload_bands = boom
with pytest.raises(RuntimeError):
r._run_image("px2", ("enc", 1, b"b"))
assert r.img_key is None # the resident outputs may be half rewritten: no hit for either image
r._upload_bands = lambda px: log.append(("upload", px))
r._run_image("px1", ("enc", 1, b"a"))
assert log[-1] == ("replay", "IMG") and "image_cache_hits" not in r.stats
def test_release_clears_key(monkeypatch):
r, log = _fake_runner(monkeypatch)
r._run_image("px1", ("enc", 1, b"a"))
r.release()
assert r.img_key is None and r.trace_img is None
# ------------------------------------------------------------------------------------------------
# app: routes with a fake pipeline (deterministic outputs from the pixels it receives)
# ------------------------------------------------------------------------------------------------
class _FakeRunner:
def __init__(self):
self.stats = {}
self.img_key = None
class _FakePipeline:
"""forward_rgb_selected_multi emulates the device runner's cache contract: a hit (same key) must return what the
resident image gives, so a wrong hit (stale pixels) shows up as a wrong answer."""
def __init__(self):
self._full = _FakeRunner()
self.calls = []
self.resident = None
self.fail = False
def prefetch_text(self, prompts):
pass
def forward_rgb_selected_multi(self, rgb, prompts, threshold, max_instances, image_key=None):
if self.fail:
raise RuntimeError("device error")
if image_key is None or image_key != self._full.img_key:
self.resident = np.ascontiguousarray(rgb).astype(np.float64)
self._full.img_key = image_key
hit = False
else:
self._full.stats["image_cache_hits"] = self._full.stats.get("image_cache_hits", 0) + 1
hit = True
self.calls.append((image_key, hit))
out = []
for k, p in enumerate(prompts):
m = float(self.resident.mean()) / 255.0
logits = torch.full((1, A.MASK_RES, A.MASK_RES), -1.0)
logits[0, 10:40 + int(50 * m), 20:60] = 1.0 + k
out.append((torch.tensor([0.5 + 0.4 * m]), torch.tensor([[0.5, 0.5, 0.2 + 0.1 * m, 0.3]]), logits, 0.9))
return out
@pytest.fixture
def client(monkeypatch):
from fastapi.testclient import TestClient
pipe = _FakePipeline()
cfg = types.SimpleNamespace(max_prompts=4, max_side=4096)
monkeypatch.setattr(A, "STATE", {"pipeline": pipe, "config": cfg, "rgb_input": True})
monkeypatch.setattr(A, "_IMG_CACHE", True)
A._LAST_DECODED[:] = [None, None]
yield TestClient(A.app), pipe
A._LAST_DECODED[:] = [None, None]
def _png(arr):
b = io.BytesIO()
Image.fromarray(arr).save(b, format="PNG")
return b.getvalue()
def _img(seed, h=97, w=131):
return np.random.default_rng(seed).integers(0, 256, size=(h, w, 3), dtype=np.uint8)
def _post_json(c, png, prompts=("car",)):
r = c.post("/predict", json={"image": base64.b64encode(png).decode(), "prompts": list(prompts)})
assert r.status_code == 200, r.text
return r.json()
def test_predict_cache_hit_same_results(client):
c, pipe = client
p1, p2 = _png(_img(1)), _png(_img(2))
a = _post_json(c, p1)
b = _post_json(c, p1, ("tree",)) # same image, other prompt: hit
assert pipe.calls[0][1] is False and pipe.calls[1][1] is True and pipe.calls[0][0] == pipe.calls[1][0]
assert a["results"][0]["instances"] == b["results"][0]["instances"]
_post_json(c, p2) # different image: miss
assert pipe.calls[2][1] is False and pipe.calls[2][0] != pipe.calls[0][0]
a2 = _post_json(c, p1) # back to the first image: miss again (one entry), same answer
assert pipe.calls[3][1] is False and a2["results"] == a["results"]
def test_predict_cache_off(client, monkeypatch):
c, pipe = client
monkeypatch.setattr(A, "_IMG_CACHE", False)
p1 = _png(_img(1))
a, b = _post_json(c, p1), _post_json(c, p1)
assert [k for k, _ in pipe.calls] == [None, None] and a["results"] == b["results"]
def test_predict_failure_invalidates(client):
c, pipe = client
p1 = _png(_img(1))
_post_json(c, p1)
assert pipe._full.img_key is not None and A._LAST_DECODED[0] is not None
pipe.fail = True
r = c.post("/predict", json={"image": base64.b64encode(p1).decode(), "prompts": ["car"]})
assert r.status_code == 500
assert pipe._full.img_key is None and A._LAST_DECODED == [None, None]
pipe.fail = False
_post_json(c, p1)
assert pipe.calls[-1][1] is False
def test_predict_response_schema_unchanged(client):
c, _ = client
j = _post_json(c, _png(_img(3)), ("car", "tree"))
assert set(j) == {"width", "height", "input_size", "threshold", "mask_format", "results", "timing_ms"}
assert set(j["timing_ms"]) == {"decode", "preprocess", "forward", "forward_per_prompt", "postprocess", "total"}
assert [r["prompt"] for r in j["results"]] == ["car", "tree"]
assert set(j["results"][0]["instances"][0]) == {"score", "box_xyxy", "mask_area", "mask_png"}
r = c.post("/predict", json={"image": "!!!", "prompts": ["car"]})
assert r.status_code == 400
r = c.post("/predict", json={"image": base64.b64encode(_png(_img(3))).decode(), "prompts": [" "]})
assert r.status_code == 400
# ------------------------------------------------------------------------------------------------
# /predict_raw (binary body)
# ------------------------------------------------------------------------------------------------
def test_predict_raw_png_and_rgb_match_predict(client):
c, pipe = client
arr = _img(5)
png = _png(arr)
j = _post_json(c, png, ("car", "tree"))
r = c.post("/predict_raw", params=[("prompts", "car"), ("prompts", "tree")], content=png,
headers={"Content-Type": "image/png"})
assert r.status_code == 200, r.text
jr = r.json()
assert jr["results"] == j["results"] and set(jr) == set(j)
assert pipe.calls[-1][1] is True # same PNG bytes as the JSON request: same key, cache hit
h, w = arr.shape[:2]
r = c.post("/predict_raw", params={"prompts": ["car", "tree"]}, content=arr.tobytes(),
headers={"Content-Type": "application/x-rgb24", "X-Width": str(w), "X-Height": str(h)})
assert r.status_code == 200, r.text
assert r.json()["results"] == j["results"] and (r.json()["width"], r.json()["height"]) == (w, h)
assert pipe.calls[-1][1] is False and pipe.calls[-1][0][0] == f"rgb{w}x{h}" # raw pixels never alias encoded bytes
def test_predict_raw_rgb_key_includes_shape(client):
c, pipe = client
arr = _img(6, 12, 12)
body = arr.tobytes()
for w, h in ((12, 12), (24, 6)): # same bytes, different shape -> different image, no hit
r = c.post("/predict_raw", params={"prompts": "car"}, content=body,
headers={"Content-Type": "application/x-rgb24", "X-Width": str(w), "X-Height": str(h)})
assert r.status_code == 200, r.text
assert [hit for _, hit in pipe.calls] == [False, False]
def test_predict_raw_errors(client):
c, _ = client
arr = _img(7)
h, w = arr.shape[:2]
hdr = {"Content-Type": "application/x-rgb24", "X-Width": str(w), "X-Height": str(h)}
assert c.post("/predict_raw", params={"prompts": "car"}, content=arr.tobytes()[:-1], headers=hdr).status_code == 400
assert c.post("/predict_raw", params={"prompts": "car"}, content=arr.tobytes(),
headers={"Content-Type": "application/x-rgb24"}).status_code == 400
assert c.post("/predict_raw", params={"prompts": "car"}, content=b"x" * 30,
headers={**hdr, "X-Width": "5000", "X-Height": "2"}).status_code == 400
assert c.post("/predict_raw", params={"prompts": "car"}, content=b"not an image",
headers={"Content-Type": "image/png"}).status_code == 400
assert c.post("/predict_raw", params={"prompts": "car"}, content=b"", headers={"Content-Type": "image/png"}).status_code == 400
assert c.post("/predict_raw", content=_png(arr), headers={"Content-Type": "image/png"}).status_code == 422
assert c.post("/predict_raw", params={"prompts": " "}, content=_png(arr), headers={"Content-Type": "image/png"}).status_code == 400
assert c.post("/predict_raw", params={"prompts": ["a"] * 5}, content=_png(arr),
headers={"Content-Type": "image/png"}).status_code == 400 # > max_prompts (4)
assert c.post("/predict_raw", params={"prompts": "car", "threshold": 2}, content=_png(arr),
headers={"Content-Type": "image/png"}).status_code == 422
def test_predict_raw_not_ready(monkeypatch):
from fastapi.testclient import TestClient
monkeypatch.setattr(A, "STATE", {})
c = TestClient(A.app)
assert c.post("/predict_raw", params={"prompts": "car"}, content=b"x", headers={"Content-Type": "image/png"}).status_code == 503
assert c.post("/predict", json={"image": "eA==", "prompts": ["car"]}).status_code == 503