File size: 5,457 Bytes
179df43 2004a78 179df43 2004a78 179df43 2004a78 179df43 2004a78 3eb7396 179df43 2004a78 179df43 a6d894c 2004a78 179df43 2004a78 179df43 2004a78 179df43 | 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 | # SPDX-FileCopyrightText: (c) 2026 Tenstorrent AI ULC
# SPDX-License-Identifier: Apache-2.0
"""Minimal pytest fixtures for tt-sam3.
Provides a ``device`` fixture and a ``sam3_model`` fixture so the tt_sam3 tests
can be run without the tt-metal monorepo's own conftest.py. The device is
selected with ``SAM3_DEVICE`` env var (default 0).
"""
from __future__ import annotations
import gc
import os
import unittest.mock as mock
import pytest
import torch
@pytest.fixture(autouse=True)
def _gc_between_tests():
gc.collect()
@pytest.fixture
def reset_seeds():
"""tt-metal's root conftest fixture the legacy tests request (deterministic random inputs)."""
torch.manual_seed(213919)
@pytest.fixture(scope="session")
def device_params(request):
"""The legacy tests parametrize ``device_params`` indirectly (``{"l1_small_size": 32768}``, tt-metal's
root conftest contract). Session-scoped like ``device`` (every test here passes the same value, so the
device is opened once per session); ``{}`` for tests that do not parametrize it."""
return getattr(request, "param", {})
@pytest.fixture(scope="session")
def device(device_params):
"""Opens the device the way the server / benchmark do (``tt_sam3.tt.fused_config.device_open_kwargs``):
bare unless the test's ``device_params`` ask for ``l1_small_size`` (the legacy PCC tests: 32768) or
``SAM3_L1_SMALL_SIZE`` overrides it (``0`` = the served bare configuration), and the trace region
whenever ``TT_FUSED=1`` selects the fused encoder trace."""
import ttnn
from tt_sam3.tt.fused_config import FusedConfig, device_open_kwargs
device_id = int(os.environ.get("SAM3_DEVICE", "0"))
env_l1 = os.environ.get("SAM3_L1_SMALL_SIZE")
l1_small = int(env_l1) if env_l1 is not None else int(device_params.get("l1_small_size", 0))
from tt_sam3.tt.device_open import open_device
# default ETH dispatch + 1 CQ + 12x10 grid (SAM3_DISPATCH=auto), as the server and the Python API
dev = open_device(device_open_kwargs(FusedConfig.from_env(), device_id=device_id, l1_small_size=l1_small),
os.environ.get("SAM3_DISPATCH", "auto"))[0]
yield dev
ttnn.close_device(dev)
def _patch_cuda_to_cpu():
"""Context-manager-free CUDA→CPU patching for SAM3 model loading."""
orig = {
n: getattr(torch, n)
for n in [
"zeros", "ones", "arange", "empty", "full", "randn",
"rand", "tensor", "linspace", "logspace", "eye",
]
}
def _redirect(fn):
def wrapper(*args, **kwargs):
if "device" in kwargs and kwargs["device"] is not None and "cuda" in str(kwargs["device"]):
kwargs["device"] = "cpu"
return fn(*args, **kwargs)
return wrapper
patches = [mock.patch("torch.cuda.is_available", return_value=False)]
for name, fn in orig.items():
patches.append(mock.patch(f"torch.{name}", _redirect(fn)))
return patches
def _resolve_pinned_checkpoint():
"""``sam3.pt`` the way the server resolves it: ``SAM3_WEIGHTS_DIR`` (offline dir) or the HF cache at
``TT_WEIGHTS_REVISION``; None when neither is set (sam3 then downloads ``facebook/sam3@main``)."""
weights_dir = os.environ.get("SAM3_WEIGHTS_DIR")
if weights_dir:
return os.path.join(weights_dir, "sam3.pt")
revision = os.environ.get("TT_WEIGHTS_REVISION")
if revision:
from huggingface_hub import hf_hub_download
return hf_hub_download(repo_id="facebook/sam3", filename="sam3.pt", revision=revision)
return None
def _build_sam3_cpu(use_pretrained=False):
# SAM3_BPE_PATH > vendored tt_sam3/assets copy > legacy open_clip path (see tt_sam3.tt.ttnn_sam3_model)
from tt_sam3.tt.ttnn_sam3_model import resolve_bpe_path
bpe_path = resolve_bpe_path()
# --pretrained honours the served pin (TT_WEIGHTS_REVISION / SAM3_WEIGHTS_DIR) instead of facebook/sam3@main
checkpoint_path = _resolve_pinned_checkpoint() if use_pretrained else None
patches = _patch_cuda_to_cpu()
for p in patches:
p.start()
try:
from sam3.model_builder import build_sam3_image_model
model = build_sam3_image_model(
bpe_path=bpe_path,
device="cpu",
eval_mode=True,
load_from_HF=bool(use_pretrained) and checkpoint_path is None,
checkpoint_path=checkpoint_path,
enable_segmentation=True,
enable_inst_interactivity=False,
)
finally:
for p in patches:
p.stop()
return model
@pytest.fixture(scope="session")
def sam3_model():
return _build_sam3_cpu(use_pretrained=False)
@pytest.fixture(scope="session")
def sam3_reference_model(sam3_model):
"""Alias the segmentation tests request (random-weight shadow, as ``sam3_model``)."""
return sam3_model
@pytest.fixture(scope="session")
def sam3_vit_backbone(sam3_model):
from sam3.model.vitdet import ViT
for _, module in sam3_model.named_modules():
if isinstance(module, ViT):
return module
raise RuntimeError("Could not find ViT backbone in SAM3 model")
@pytest.fixture(scope="session")
def sam3_neck(sam3_model):
from sam3.model.necks import Sam3DualViTDetNeck
for _, module in sam3_model.named_modules():
if isinstance(module, Sam3DualViTDetNeck):
return module
raise RuntimeError("Could not find neck in SAM3 model")
|