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: ...@@ -2517,6 +2517,36 @@ class MultiModelManager:
return str(target) return str(target)
return None 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): 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. """Store a model's config under its type key, and under its ALIAS too.
...@@ -5705,7 +5735,25 @@ class MultiModelManager: ...@@ -5705,7 +5735,25 @@ class MultiModelManager:
pass pass
# Step 2: Build the model key (prefixed with type) # 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}" model_key = f"{model_type}:{resolved_name}"
else: else:
model_key = resolved_name model_key = resolved_name
......
...@@ -8,6 +8,8 @@ the wrong model, silently, because both are "valid" configs for that path. ...@@ -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 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. weights file, 178000 ctx vs 32768). The config used to LOAD did not.
""" """
import pytest
from codai.models.manager import MultiModelManager from codai.models.manager import MultiModelManager
...@@ -138,3 +140,79 @@ def test_every_type_setter_uses_the_shared_store(): ...@@ -138,3 +140,79 @@ def test_every_type_setter_uses_the_shared_store():
src) src)
assert not leftovers, leftovers assert not leftovers, leftovers
assert src.count("_store_type_config(") >= 9 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