product-recognition-demo/recognization/code/test_gallery.py
kiyo9w 3614b23a2c fix: serialize gallery inference so Metal cannot be entered twice
The server segfaulted a few seconds into every live scan that used the crop
lane. The crash report put the fault in libtorch's Metal shader library
(`MetalShaderLibrary::exec_unary_kernel` inside an unordered_map insert),
reached from `copy_cast_kernel_mps` under `clone`/`reshape`.

Reproduced deterministically: eight threads calling `embed()` on a fresh
process crash it, while the same eight threads after one warm forward are
fine. Metal's shader library is process-global and is not thread-safe while
kernels are still being compiled into it, which is exactly what the first
forward does. The detector already serializes its predict for this reason;
the gallery did not.

Two changes:

* A module-level `_MPS_INFER_LOCK` covers the whole MPS region of a forward -
  model load, `.to(device)`, the forward, and the normalize that follows it.
  Normalize dispatches its own Metal kernels, so leaving it outside the lock
  left the same first-compile race open. Module-level because the state being
  protected is the process's, not one gallery's.
* A cache hit skips the embedding forward, so the model used to be built and
  the kernels compiled by whichever classify worker asked first - up to eight
  racing into one process-wide cache. The load path now warms the backbone
  single-threaded under the existing lock, on the cache-hit branch too. The
  readiness path loads the gallery before the socket accepts, so the warmup
  is free. It is Metal-only and best-effort: a CPU gallery has no shader
  library to compile, and a cache hit is served without a usable backbone by
  contract.

The regression test records overlap rather than timing: a fake backbone that
sleeps and counts concurrent entries, run from four threads, asserting the
count never exceeds one. It fails on the previous gallery.py and needs no MPS.
2026-09-28 17:22:55 +09:00

631 lines
27 KiB
Python

"""
Tests for the product gallery's matching logic.
The embedding model is injected, so these run without torch or a GPU: what is
being tested is the retrieval contract - best view wins, several views per SKU
count as one answer, and a stranger is rejected rather than named.
"""
from __future__ import annotations
import os
import sys
import unittest
import numpy as np
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
if os.path.exists(os.path.join(project_root, "stock")):
os.chdir(project_root)
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
from gallery import ( # noqa: E402
DINOV2_MODEL,
DINOV2_SHA256,
GalleryEntry,
ProductGallery,
catalog_digest,
dinov2_weights_path,
head_class_set,
load_dinov2,
)
import gallery as gallery_module # noqa: E402
def a_gallery(views, threshold=0.55):
"""A gallery with hand-made embeddings: no model, no files."""
skus = sorted({sku for sku, _ in views})
gallery = ProductGallery("does-not-exist", skus, threshold=threshold)
embeddings = []
entries = []
for index, (sku, vector) in enumerate(views):
vector = np.asarray(vector, dtype=np.float32)
vector /= np.linalg.norm(vector)
embeddings.append(vector)
entries.append(GalleryEntry(sku_id=sku, view=f"{sku}_{index}", embedding=vector))
gallery._entries = entries
gallery._matrix = np.stack(embeddings)
gallery._available = True
return gallery
def unit(vector):
vector = np.asarray(vector, dtype=np.float32)
return (vector / np.linalg.norm(vector)).tolist()
class Backbone:
"""A deterministic stand-in for DINOv2: the prepared crop's channel means,
tiled to the width the real backbone returns."""
def __init__(self):
self.calls = 0
def __call__(self, tensor):
self.calls += 1
return tensor.mean(dim=(2, 3)).repeat(1, gallery_module.DINOV2_EMBED_DIM // 3)
def a_query():
"""A unit vector in the backbone's space, for match() without a model."""
query = np.zeros(gallery_module.DINOV2_EMBED_DIM, dtype=np.float32)
query[0] = 1.0
query[1] = 0.05
return query / np.linalg.norm(query)
class UnreachableBackbone:
def __call__(self, tensor):
raise AssertionError("the gallery loaded a backbone it should have cached")
def write_view(path, seed):
import cv2
image = np.random.RandomState(seed).randint(0, 256, (24, 32, 3), dtype=np.uint8)
cv2.imwrite(path, image)
def a_store(directory, names=("sku_a", "sku_a_1", "sku_b")):
"""A store dir: one JPEG per view plus the stock.csv the catalog is read from."""
with open(os.path.join(directory, "stock.csv"), "w", encoding="utf-8") as handle:
handle.write("SKU_ID,product_name\n")
for index, name in enumerate(names):
write_view(os.path.join(directory, f"{name}.jpeg"), index)
def a_stored_gallery(directory, names, backbone, image_size=32):
gallery = ProductGallery(directory, list(names), device="cpu", image_size=image_size)
gallery._load_model = lambda: backbone
return gallery
class MatchingTests(unittest.TestCase):
def test_the_nearest_view_names_the_product(self):
gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0]))])
gallery.embed = lambda crop: np.array([0.9, 0.1, 0.0], dtype=np.float32)
match = gallery.match(object())
self.assertFalse(match.rejected)
self.assertEqual(match.sku_id, "sku_a")
self.assertAlmostEqual(match.similarity, 0.9, places=3)
def test_several_views_of_one_sku_are_one_answer(self):
# The second view of sku_b is closer than any view of sku_a, but sku_a must
# still win on its best view: a SKU is not outvoted by having more photos.
gallery = a_gallery([
("sku_a", unit([1, 0, 0])),
("sku_b", unit([0, 1, 0])),
("sku_b", unit([0.2, 0.98, 0])),
])
gallery.embed = lambda crop: np.array([1.0, 0.05, 0.0], dtype=np.float32)
match = gallery.match(object())
self.assertEqual(match.sku_id, "sku_a")
self.assertEqual(match.runner_up, "sku_b")
def test_a_stranger_is_rejected_not_named(self):
gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0]))])
gallery.embed = lambda crop: np.array([0.0, 0.0, 1.0], dtype=np.float32)
match = gallery.match(object())
self.assertTrue(match.rejected)
self.assertIsNone(match.sku_id)
def test_the_threshold_moves_the_rejection_line(self):
gallery = a_gallery([("sku_a", unit([1, 0, 0]))], threshold=0.99)
gallery.embed = lambda crop: np.array([0.9, 0.4, 0.0], dtype=np.float32)
self.assertTrue(gallery.match(object()).rejected)
self.assertFalse(gallery.match(object(), threshold=0.5).rejected)
def test_margin_separates_a_confident_match_from_a_close_one(self):
gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0.9, 0.44, 0]))])
gallery.embed = lambda crop: np.array([1.0, 0.1, 0.0], dtype=np.float32)
confident = gallery.match(object())
gallery.embed = lambda crop: np.array([0.95, 0.3, 0.0], dtype=np.float32)
close = gallery.match(object())
self.assertGreater(confident.margin, close.margin)
def test_the_per_sku_max_does_not_depend_on_entry_order(self):
# Three SKUs, several views each, enrolled out of order: a SKU is scored
# by its best view, and the answer cannot move because a row moved.
gallery = a_gallery([
("sku_b", unit([0, 1, 0])),
("sku_a", unit([1, 0, 0])),
("sku_c", unit([0, 0, 1])),
("sku_b", unit([0.3, 0.95, 0])),
("sku_a", unit([0.98, 0.2, 0])),
])
gallery.embed = lambda crop: np.array([1.0, 0.05, 0.0], dtype=np.float32)
straight = gallery.match(object())
self.assertEqual(straight.sku_id, "sku_a")
self.assertEqual(straight.runner_up, "sku_b")
for order in ([4, 0, 3, 1, 2], [2, 3, 1, 4, 0]):
gallery._entries = [gallery._entries[index] for index in order]
gallery._matrix = gallery._matrix[order]
shuffled = gallery.match(object())
self.assertEqual(shuffled.sku_id, straight.sku_id)
self.assertEqual(shuffled.runner_up, straight.runner_up)
self.assertAlmostEqual(shuffled.similarity, straight.similarity, places=6)
self.assertAlmostEqual(shuffled.runner_up_similarity, straight.runner_up_similarity, places=6)
def test_a_tie_keeps_the_first_enrolled_sku(self):
# Exactly equidistant: first appearance decides, as it did before the
# per-SKU maximum became a segmented reduction.
gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0]))])
gallery.embed = lambda crop: np.array([1.0, 1.0, 0.0], dtype=np.float32)
match = gallery.match(object())
self.assertFalse(match.rejected)
self.assertEqual(match.sku_id, "sku_a")
self.assertEqual(match.runner_up, "sku_b")
self.assertAlmostEqual(match.similarity, match.runner_up_similarity, places=6)
self.assertAlmostEqual(match.margin, 0.0, places=6)
def test_an_unloaded_gallery_rejects_everything(self):
gallery = ProductGallery("does-not-exist", ["sku_a"])
match = gallery.match(object())
self.assertTrue(match.rejected)
class EnrollmentTests(unittest.TestCase):
def test_views_are_found_by_sku_prefix(self):
import tempfile
with tempfile.TemporaryDirectory() as directory:
for name in ("sku_a.jpeg", "sku_a_1.jpeg", "sku_a-2.jpeg", "sku_ab.jpeg", "notes.txt"):
open(os.path.join(directory, name), "wb").close()
gallery = ProductGallery(directory, ["sku_a", "sku_ab"])
self.assertEqual(len(gallery._views_for("sku_a")), 3,
"one view, two extra views, and not sku_ab")
self.assertEqual(len(gallery._views_for("sku_ab")), 1)
class FeatureCacheTests(unittest.TestCase):
"""The cached matrix must be this store's, or it must not be used."""
def setUp(self):
import hashlib
import tempfile
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
a_store(self.directory.name)
# A cache is only served when the pinned backbone is on disk, so point the
# pin at a stand-in file the module's expected digest is told about. The
# check itself - hash this file, compare - is what is under test.
self.weights = os.path.join(self.directory.name, "dinov2.safetensors")
self.write(self.weights, b"not really a checkpoint")
os.environ["DORAYAKI_DINOV2_WEIGHTS"] = self.weights
self.addCleanup(os.environ.pop, "DORAYAKI_DINOV2_WEIGHTS", None)
self.addCleanup(setattr, gallery_module, "DINOV2_SHA256", gallery_module.DINOV2_SHA256)
gallery_module.DINOV2_SHA256 = hashlib.sha256(b"not really a checkpoint").hexdigest()
def write(self, path, content):
with open(path, "wb") as handle:
handle.write(content)
return path
def real_gallery(self):
"""A gallery with the real _load_model: the server's own readiness path."""
return ProductGallery(self.directory.name, ["sku_a", "sku_b"], device="cpu", image_size=32)
def loaded(self, names=("sku_a", "sku_b"), image_size=32):
backbone = Backbone()
gallery = a_stored_gallery(self.directory.name, names, backbone, image_size=image_size)
self.assertTrue(gallery.load())
return gallery, backbone
def cache_payload(self, gallery):
"""The rows and matrix a loaded gallery left on disk."""
with np.load(gallery._cache_path(), allow_pickle=False) as payload:
return (
[str(value) for value in payload["skus"].tolist()],
[str(value) for value in payload["views"].tolist()],
payload["matrix"],
)
def rewrite_cache(self, gallery, skus, views, matrix):
"""The same valid key with different contents: only the rows can catch it."""
path = gallery._cache_path()
with np.load(path, allow_pickle=False) as payload:
key = str(payload["key"].item())
np.savez(path, key=key, skus=np.array(skus), views=np.array(views), matrix=matrix)
def re_embeds(self, first, why):
"""The one right answer to a rewritten cache: build it from the pixels."""
backbone = Backbone()
gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone)
self.assertTrue(gallery.load(), why)
self.assertEqual(backbone.calls, 1, f"{why} must not be served from cache")
np.testing.assert_array_equal(gallery._matrix, first._matrix)
self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_a", "sku_b"])
def test_a_second_start_answers_the_same_from_the_cache(self):
first, backbone = self.loaded()
self.assertEqual(backbone.calls, 1)
self.assertEqual(first.view_count(), 3)
self.assertTrue(os.path.isfile(first._cache_path()))
cached = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), UnreachableBackbone())
self.assertTrue(cached.load())
self.assertEqual(cached.view_count(), first.view_count())
np.testing.assert_array_equal(cached._matrix, first._matrix)
self.assertEqual([entry.view for entry in cached._entries],
[entry.view for entry in first._entries])
query = a_query()
first.embed = cached.embed = lambda crop: query
self.assertEqual(vars(cached.match(object())), vars(first.match(object())))
def test_changed_view_bytes_re_embed(self):
first, _ = self.loaded()
before = first._matrix.copy()
write_view(os.path.join(self.directory.name, "sku_b.jpeg"), seed=99)
second, backbone = self.loaded()
self.assertEqual(backbone.calls, 1, "a replaced view must not be served from cache")
self.assertFalse(np.array_equal(second._matrix, before))
def test_a_reordered_catalog_does_not_borrow_another_mapping(self):
self.loaded()
swapped, backbone = self.loaded(names=("sku_b", "sku_a"))
self.assertEqual(backbone.calls, 1)
self.assertEqual([entry.sku_id for entry in swapped._entries], ["sku_b", "sku_a", "sku_a"])
def test_another_image_size_is_never_served(self):
small, _ = self.loaded(image_size=32)
large, backbone = self.loaded(image_size=64)
self.assertEqual(backbone.calls, 1)
self.assertNotEqual(small._cache_path(), large._cache_path())
def test_a_corrupt_cache_re_embeds_the_same_matrix(self):
first, _ = self.loaded()
with open(first._cache_path(), "wb") as handle:
handle.write(b"not an npz at all")
second, backbone = self.loaded()
self.assertEqual(backbone.calls, 1)
np.testing.assert_array_equal(second._matrix, first._matrix)
def test_swapped_cache_rows_are_not_served(self):
# A current key says nothing about the rows inside the file. The two
# sku_a views are adjacent, so a swap has to cross SKUs to mean
# anything - and the matrix that moves with them is the dangerous part:
# a vector that would then be filed under the wrong product.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.assertEqual(list(zip(skus, views)),
[("sku_a", "sku_a.jpeg"), ("sku_a", "sku_a_1.jpeg"), ("sku_b", "sku_b.jpeg")])
self.rewrite_cache(first, skus[::-1], views[::-1], matrix[::-1])
self.re_embeds(first, "rows swapped with their vectors")
def test_mislabelled_cache_rows_are_not_served(self):
# The views and the matrix stay where they are, so every row names a
# product it was not embedded for.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, [skus[2], skus[1], skus[0]], views, matrix)
self.re_embeds(first, "labels swapped around the views")
def test_a_duplicated_cache_row_is_not_served(self):
# The first view written twice: one image would answer for two products.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, skus[:1] * 2 + skus[2:], views[:1] * 2 + views[2:], matrix)
self.re_embeds(first, "a row repeated")
def test_a_foreign_sku_row_is_not_served(self):
# A row from another store's catalog: a SKU this gallery never enrolled.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, skus[1:] + ["other_store"], views[1:] + ["other_store.jpeg"], matrix)
self.re_embeds(first, "another store's row")
def test_a_cache_may_omit_a_view_it_could_not_decode(self):
# The writer drops a view it could not read and keeps everyone else's
# order, so such a cache is still this store's - and still worth serving
# rather than re-embedding the whole catalog on every start.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, skus[:1] + skus[2:], views[:1] + views[2:], matrix[[0, 2]])
backbone = Backbone()
gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone)
self.assertTrue(gallery.load())
self.assertEqual(backbone.calls, 0, "a readable shorter cache is still a hit")
self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_b"])
np.testing.assert_array_equal(gallery._matrix, first._matrix[[0, 2]])
def test_a_nonnumeric_cache_matrix_is_not_served(self):
# Only the float matrix _cache_write produces is a cache. A complex
# matrix is finite and casts without complaint, so it answers with a
# different matrix than the one on disk; integer, boolean and text
# matrices are not embeddings at all.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
others = {
"complex": matrix.astype(np.complex128),
"timedelta": np.zeros_like(matrix, dtype="timedelta64[s]"),
"integer": (matrix * 8).astype(np.int32),
"boolean": np.ones_like(matrix, dtype=bool),
"text": np.zeros(matrix.shape, dtype="<U4"),
}
for name, other in others.items():
with self.subTest(matrix=name):
self.rewrite_cache(first, skus, views, other)
self.re_embeds(first, f"a {name} matrix")
def test_a_loaded_gallery_reloads_when_its_source_moves(self):
gallery, backbone = self.loaded()
self.assertTrue(gallery.load())
self.assertEqual(backbone.calls, 1, "an unchanged store must not re-embed")
write_view(os.path.join(self.directory.name, "sku_b.jpeg"), seed=99)
self.assertTrue(gallery.load())
self.assertEqual(backbone.calls, 2)
with open(os.path.join(self.directory.name, "stock.csv"), "a", encoding="utf-8") as handle:
handle.write("sku_c,new product\n")
self.assertTrue(gallery.load())
self.assertEqual(backbone.calls, 3, "stock.csv is part of what an embedding is pinned to")
self.assertEqual(gallery.view_count(), 3)
def test_a_valid_cache_needs_the_pinned_backbone_on_disk(self):
# The regression: a cache hit never opens load_dinov2, so a deleted or
# altered checkpoint used to read as a loaded gallery and then raise on
# the first match - after the server had reported itself ready.
self.loaded()
gallery = self.real_gallery()
self.assertTrue(gallery.load(), "the cache serves while the pin holds")
os.environ["DORAYAKI_DINOV2_WEIGHTS"] = os.path.join(self.directory.name, "gone.safetensors")
missing = self.real_gallery()
self.assertFalse(missing.load())
self.assertFalse(missing.available())
os.environ["DORAYAKI_DINOV2_WEIGHTS"] = self.write(
os.path.join(self.directory.name, "altered.safetensors"), b"a different checkpoint"
)
altered = self.real_gallery()
self.assertFalse(altered.load())
self.assertFalse(altered.available())
def test_an_altered_backbone_re_embeds_rather_than_serves(self):
# With a backbone that can still load, an altered file must re-embed.
self.loaded()
self.assertTrue(
a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), UnreachableBackbone()).load(),
"the cache still serves on the pinned file",
)
self.write(self.weights, b"a different checkpoint")
backbone = Backbone()
rebuilt = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone)
self.assertTrue(rebuilt.load())
self.assertEqual(backbone.calls, 1, "an unpinned cache must not be served")
class OfflineWeightsTests(unittest.TestCase):
def test_a_missing_weights_file_names_the_path_and_does_not_fetch(self):
previous = os.environ.get("DORAYAKI_DINOV2_WEIGHTS")
os.environ["DORAYAKI_DINOV2_WEIGHTS"] = os.path.join(
"does-not-exist", "dinov2_vits14_lvd142m.safetensors"
)
try:
with self.assertRaises(FileNotFoundError) as raised:
load_dinov2(224)
finally:
if previous is None:
os.environ.pop("DORAYAKI_DINOV2_WEIGHTS", None)
else:
os.environ["DORAYAKI_DINOV2_WEIGHTS"] = previous
message = str(raised.exception)
self.assertIn("does-not-exist", message)
self.assertIn(DINOV2_SHA256, message)
self.assertIn("does not download", message)
def test_the_default_path_stays_inside_the_repo(self):
previous = os.environ.pop("DORAYAKI_DINOV2_WEIGHTS", None)
try:
self.assertTrue(dinov2_weights_path().endswith(
os.path.join("weights", "dinov2_vits14_lvd142m.safetensors")
))
finally:
if previous is not None:
os.environ["DORAYAKI_DINOV2_WEIGHTS"] = previous
def _head_payload(class_ids, torch):
projection = torch.nn.Linear(384, 128)
prototypes = torch.nn.functional.normalize(torch.randn(len(class_ids), 128), dim=1)
return {
"class_ids": list(class_ids),
"image_size": 518,
"base_model": DINOV2_MODEL,
"projection": {key: value.detach().cpu() for key, value in projection.state_dict().items()},
"prototypes": prototypes.detach().cpu(),
}
class HeadPinTests(unittest.TestCase):
def test_a_stale_head_cannot_label_this_catalog(self):
import tempfile
import torch
with tempfile.TemporaryDirectory() as directory:
with open(os.path.join(directory, "stock.csv"), "w", encoding="utf-8") as handle:
handle.write("SKU_ID,product_name\nsku_a,A\nsku_b,B\nsku_c,C\n")
gallery = ProductGallery(directory, ["sku_a", "sku_b", "sku_c"])
gallery._available = True
path = gallery._head_path
subset = ["sku_c", "sku_a"]
digest = catalog_digest(directory, subset)
foreign = _head_payload(["other_store", "sku_a"], torch)
torch.save(foreign, path)
self.assertFalse(gallery.load_head())
self.assertIsNone(gallery._head)
self.assertIn("catalog_mismatch", gallery.head_reason)
self.assertTrue(gallery.available())
legacy = _head_payload(subset, torch)
torch.save(legacy, path)
self.assertTrue(gallery.load_head())
self.assertEqual(gallery.head_reason, "legacy_unpinned")
self.assertEqual(gallery._head["class_ids"], subset)
self.assertNotIn("other_store", gallery._head["class_ids"])
stale = _head_payload(subset, torch)
stale["catalog_digest"] = "0" * 64
torch.save(stale, path)
self.assertFalse(gallery.load_head())
self.assertIsNone(gallery._head)
self.assertIn("catalog_mismatch", gallery.head_reason)
self.assertTrue(gallery.available())
wrong_backbone = _head_payload(subset, torch)
wrong_backbone["catalog_digest"] = digest
wrong_backbone["base_model"] = "vit_base_patch14_dinov2.lvd142m"
torch.save(wrong_backbone, path)
self.assertFalse(gallery.load_head())
self.assertIn("backbone_mismatch", gallery.head_reason)
wrong_size = _head_payload(subset, torch)
wrong_size["catalog_digest"] = digest
wrong_size["image_size"] = 224
torch.save(wrong_size, path)
self.assertFalse(gallery.load_head())
self.assertIn("image_size_mismatch", gallery.head_reason)
torch.save(["not", "a", "head"], path)
self.assertFalse(gallery.load_head())
self.assertTrue(gallery.head_reason.startswith("malformed"))
self.assertTrue(gallery.available())
self.assertIsNone(gallery._head)
current = _head_payload(subset, torch)
current["catalog_digest"] = digest
torch.save(current, path)
self.assertTrue(gallery.load_head())
self.assertEqual(gallery.head_reason, "ok")
self.assertEqual(gallery._head["class_ids"], subset)
self.assertIsNone(head_class_set(["sku_a", "sku_a", ""]))
class ConcurrentInferenceTests(unittest.TestCase):
"""The backbone is never entered by two threads at once.
Metal's shader library is process-global and is not thread-safe while
kernels are still being compiled into it, so two threads reaching the first
forward together segfault the whole server. This is a contract test, not a
timing test: it records overlap rather than measuring speed.
"""
def test_two_threads_never_enter_the_backbone_together(self):
import time
import threading
import tempfile
with tempfile.TemporaryDirectory() as directory:
a_store(directory, names=("sku_a", "sku_b"))
gallery = ProductGallery(directory, ["sku_a", "sku_b"], device="cpu", image_size=32)
inside = 0
overlapped = []
gate = threading.Lock()
class SlowBackbone:
def __call__(self, tensor):
nonlocal inside
with gate:
inside += 1
if inside > 1:
overlapped.append(inside)
time.sleep(0.01)
with gate:
inside -= 1
return tensor.mean(dim=(2, 3)).repeat(1, gallery_module.DINOV2_EMBED_DIM // 3)
gallery._load_model = lambda: SlowBackbone()
crop = np.zeros((24, 32, 3), dtype=np.uint8)
errors = []
def work():
try:
for _ in range(6):
gallery.embed(crop)
except Exception as error: # pragma: no cover - reported below
errors.append(repr(error))
threads = [threading.Thread(target=work) for _ in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
self.assertEqual(errors, [])
self.assertEqual(overlapped, [], "two threads ran the backbone at the same time")
class TrainerPinTests(unittest.TestCase):
def test_head_training_loads_the_pinned_file_and_keeps_preprocess(self):
import finetune_identity
import gallery as gallery_module
seen = []
class FakeBackbone:
def eval(self):
return self
def to(self, device):
return self
sentinel = FakeBackbone()
def fake_load(image_size):
seen.append(image_size)
return sentinel
# build_model resolves the loader from gallery at call time, so that is
# the attribute to replace; finetune_identity has no module-level copy.
original = gallery_module.load_dinov2
gallery_module.load_dinov2 = fake_load
try:
model, device = finetune_identity.build_model(224)
finally:
gallery_module.load_dinov2 = original
self.assertIs(model, sentinel)
self.assertIn(device, ("cpu", "mps"))
self.assertEqual(seen, [224])
image = np.zeros((10, 20, 3), dtype=np.uint8)
self.assertEqual(finetune_identity.prepare(image, 32).shape, gallery_module._prepare(image, 32).shape)
self.assertTrue(np.allclose(
finetune_identity.prepare(image, 32),
gallery_module._prepare(image, 32),
))
if __name__ == "__main__":
unittest.main()