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")