give two models sharing a weights path their own cache slot

The silent one. self.models is keyed by model_key, and model_key was the
weights path -- so two entries naming the same path shared ONE resident
pipeline. LTX-2.3-PinkCherry requested while LTX-2.3-10Eros was resident never
loaded its own transformer: no load, no eviction, straight to generation, and
HTTP 200 with 10Eros weights. Every other instance of this family failed
loudly; this one answered successfully with the wrong model.

It also meant both wrote measured_vram_gb to the same key, so a Q4_K_M and a
Q5_K_M of different sizes were overwriting each other sizing.

A key is split only when the caller named something, two or more DISTINCT
aliases point at that same path, and the named one is among them. A lone
aliased model, or a request that names the path, keeps the key it has always
had.

That narrowness is the point, not caution for its own sake: collapsing every
name form onto one key is deliberate a few lines above -- without it two
concurrent first-requests under different spellings each load an instance and
exceed max_model_instances=1. This is not a licence to key by whatever the
caller typed.

Verified live: the estimator now reports key=video:LTX-2.3-10Eros where both
models previously shared key=video:diffusers/LTX-2.3-Diffusers.

Suite: 15 failed, 5268 passed -- the same 15 as the baseline.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
parent 5aabd2fc
......@@ -2517,6 +2517,36 @@ class MultiModelManager:
return str(target)
return None
def _sibling_model_key(self, model_type, resolved_name, requested_id):
"""A per-entry key when several configs share one weights path.
Returns ``"<kind>:<alias>"`` when ALL of:
* the caller named something (``requested_id``),
* two or more distinct aliases point at ``resolved_name``,
* ``requested_id`` is one of them.
Otherwise None, and the caller keeps the ordinary path-based key. That
narrowness is the point: one model with an alias, or a request that
names the path, must behave exactly as before.
"""
if not requested_id:
return None
want = str(resolved_name or "").strip()
if not want:
return None
aliases = set()
for _k, _c in (self.config or {}).items():
if not isinstance(_c, dict):
continue
target = str(_c.get("path") or _c.get("model_path") or "").strip()
alias = str(_c.get("alias") or "").strip()
if alias and target == want:
aliases.add(alias)
if len(aliases) < 2 or requested_id not in aliases:
return None
return f"{(model_type or 'text')}:{requested_id}"
def _store_type_config(self, kind: str, model_name: str, config: Dict = None):
"""Store a model's config under its type key, and under its ALIAS too.
......@@ -5705,7 +5735,25 @@ class MultiModelManager:
pass
# Step 2: Build the model key (prefixed with type)
if model_type and model_type != "text":
#
# Siblings first: when two or more config entries name the SAME weights
# path, one key means one entry in self.models, so the second model is
# served by the first one's resident pipeline -- HTTP 200 with the
# wrong weights, silently. Measured 2026-10-27: LTX-2.3-PinkCherry
# requested while LTX-2.3-10Eros was resident never loaded its own
# transformer and generated from 10Eros's. It also meant both wrote
# measured_vram_gb to the same key and corrupted each other's sizing.
#
# Only a real collision splits the key, and only when the caller named
# an alias: a lone model, or a request that names the path, keeps the
# key it has always had. That matters because collapsing every name
# form to ONE key is deliberate elsewhere (see the GGUF note above) --
# this is not a licence to key by whatever the caller typed.
_sib_key = self._sibling_model_key(model_type, resolved_name,
_requested_id)
if _sib_key:
model_key = _sib_key
elif model_type and model_type != "text":
model_key = f"{model_type}:{resolved_name}"
else:
model_key = resolved_name
......
......@@ -8,6 +8,8 @@ the wrong model, silently, because both are "valid" configs for that path.
The VRAM estimator already disambiguated this way for lisa vs lisa-32k (same
weights file, 178000 ctx vs 32768). The config used to LOAD did not.
"""
import pytest
from codai.models.manager import MultiModelManager
......@@ -138,3 +140,79 @@ def test_every_type_setter_uses_the_shared_store():
src)
assert not leftovers, leftovers
assert src.count("_store_type_config(") >= 9
# ------------------------------------------------------- the loaded-model key
def _sibs():
m = object.__new__(MultiModelManager)
m.config = {
"video:diffusers/LTX-2.3-Diffusers": {
"alias": "LTX-2.3-PinkCherry", "path": "diffusers/LTX-2.3-Diffusers"},
"video:LTX-2.3-PinkCherry": {
"alias": "LTX-2.3-PinkCherry", "path": "diffusers/LTX-2.3-Diffusers"},
"video:LTX-2.3-10Eros": {
"alias": "LTX-2.3-10Eros", "path": "diffusers/LTX-2.3-Diffusers"},
"video:solo": {"alias": "solo", "path": "org/solo"},
}
return m
def test_siblings_get_their_own_cache_key():
"""The silent one: one key meant one resident pipeline for two models.
PinkCherry requested while 10Eros was resident never loaded its own
transformer -- it generated from 10Eros's weights and answered 200.
"""
m = _sibs()
a = m._sibling_model_key("video", "diffusers/LTX-2.3-Diffusers",
"LTX-2.3-10Eros")
b = m._sibling_model_key("video", "diffusers/LTX-2.3-Diffusers",
"LTX-2.3-PinkCherry")
assert a == "video:LTX-2.3-10Eros"
assert b == "video:LTX-2.3-PinkCherry"
assert a != b, "two models must not share one self.models slot"
def test_a_lone_model_keeps_the_path_key():
# Splitting every aliased model would undo the deliberate collapse of all
# name forms onto one key (one instance pool, no duplicate loads).
m = _sibs()
assert m._sibling_model_key("video", "org/solo", "solo") is None
def test_a_request_naming_the_path_is_not_split():
m = _sibs()
assert m._sibling_model_key("video", "diffusers/LTX-2.3-Diffusers",
"diffusers/LTX-2.3-Diffusers") is None
def test_an_unknown_id_is_not_split():
m = _sibs()
assert m._sibling_model_key("video", "diffusers/LTX-2.3-Diffusers",
"something-else") is None
@pytest.mark.parametrize("rid", [None, "", " "])
def test_no_requested_id_means_no_split(rid):
m = _sibs()
assert m._sibling_model_key("video", "diffusers/LTX-2.3-Diffusers", rid) is None
def test_model_path_counts_as_a_shared_target():
m = object.__new__(MultiModelManager)
m.config = {
"audio:a": {"alias": "a", "model_path": "/g/shared.gguf"},
"audio:b": {"alias": "b", "model_path": "/g/shared.gguf"},
}
assert m._sibling_model_key("audio", "/g/shared.gguf", "a") == "audio:a"
def test_request_model_consults_the_sibling_key_first():
import inspect
src = inspect.getsource(MultiModelManager.request_model)
i = src.index("# Step 2: Build the model key")
window = src[i:i + 1400]
assert "_sibling_model_key" in window
# and it must come BEFORE the ordinary path key, or it never applies
assert window.index("_sibling_model_key") < window.index(
'model_key = f"{model_type}:{resolved_name}"')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment