From 3f3671fbe5fe6ee940dff97b40f770c660ae5eea Mon Sep 17 00:00:00 2001 From: Yaros Date: Tue, 20 Oct 2026 22:43:55 +0200 Subject: [PATCH] fix(ml): preloaded model loaded twice (#32610) fix(ml): reuse cached model across names that clean to the same model (cherry picked from commit 166e93559f123313cf9cb4e7324c4ee30e21c618) --- machine-learning/immich_ml/models/cache.py | 4 +++- machine-learning/test_main.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/machine-learning/immich_ml/models/cache.py b/machine-learning/immich_ml/models/cache.py index db2ae46e6..580631d20 100644 --- a/machine-learning/immich_ml/models/cache.py +++ b/machine-learning/immich_ml/models/cache.py @@ -3,6 +3,7 @@ from collections.abc import Hashable from typing import Any from immich_ml import allocator +from immich_ml.config import clean_name from immich_ml.models.base import InferenceEntry, InferenceModel from immich_ml.schemas import Options @@ -15,7 +16,8 @@ class ModelCache: self._expiries: dict[Key, asyncio.TimerHandle] = {} def get[O: Options](self, entry: InferenceEntry[O], ttl: int | None = None) -> InferenceModel[O]: - key = (entry.model, entry.name, entry.model.graph(entry.options)) + # names that differ only in what cleaning drops, like an `immich-app/` prefix, are the same model + key = (entry.model, clean_name(entry.name), entry.model.graph(entry.options)) if key not in self._models: self._models[key] = entry.model.create(entry.name, entry.options) elif key not in self._expiries: diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index 5a0a869cc..d9f029093 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -1856,6 +1856,14 @@ class TestCache: assert len(model_cache._models) == 1 model.create.assert_called_once() + async def test_shares_an_instance_across_names_of_the_same_model(self) -> None: + model_cache = ModelCache() + bare = model_cache.get(InferenceEntry(OpenClipTextualEncoder, "ViT-B-32__openai", TextualOptions())) + prefixed = model_cache.get( + InferenceEntry(OpenClipTextualEncoder, "immich-app/ViT-B-32__openai", TextualOptions(language="en-US")) + ) + assert bare is prefixed + async def test_separate_instances_per_graph_option(self) -> None: # the real TextDetector, so that keying it on anything but its resolution fails this model_cache = ModelCache()