size a model by the card that was asked for, and see every kind of load

Two bugs of the same shape: a signal that already existed and was not consulted.

Same-path siblings. Two cards can share one weights path and differ only in
settings -- Wan2.2-I2V-A14B-Lightning and the plain Wan2.2-I2V-A14B both point
at Wan-AI/Wan2.2-I2V-A14B-Diffusers, declaring 22 GB and 151 GB. The manager
has disambiguated exactly this since the lisa/lisa-32k case, through
`requested_id`, and NEITHER placement caller passed it. So the Lightning card
was sized by its 151 GB sibling, read as ~127 GB over free VRAM -- far outside
the borderline band -- and went straight to CPU offload without trying the
card, where it computed at 0% GPU for 26 minutes holding 37 GB of host RAM.
Measured: 97.4 GB without the id, 14.1 GB with it, which is the difference
between exile and residency on a 24 GiB card. Quantizing the experts was
necessary but not sufficient; the ESTIMATE was still the wrong card's.

The leak watcher's load signal. _load_in_progress() consulted only the
manager's ready event, which diffusers image/video loads never touch -- they
load in codai/api/{images,video}.py. A 12-minute multi-shard load therefore
looked idle, its monotonically rising RSS looked exactly like a leak, and the
mitigation ladder ran against a load doing nothing wrong ("RSS 19.8/43.0 GB
LEAK SUSPECTED", every poll). It now also consults the loading reservations,
which cover those paths by construction.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent b3aaec96
......@@ -842,7 +842,12 @@ def _load_diffusers_pipeline(model_name: str, global_args, model_config: dict =
if _k in _mmm.config:
_key = _k
break
_need_gb = _mmm._get_model_used_vram_gb(_key or model_name, model_name)
# requested_id: same-path siblings (one checkpoint, two cards
# with different settings) must be sized by the card that was
# ASKED for, not by whichever one registered the path.
_need_gb = _mmm._get_model_used_vram_gb(
_key or model_name, model_name,
requested_id=getattr(request, 'model', None))
if _need_gb > 0:
if _free_gb < _need_gb:
print(f"Image model needs {_need_gb:.1f} GB, {_free_gb:.1f} GB free "
......
......@@ -5103,8 +5103,21 @@ async def video_generations(request: VideoGenerationRequest,
# it already includes the runtime/activation reserve AND the
# fused acceleration LoRA (it's measured after fusion). So do
# NOT re-add those. Per-request LoRAs are extra.
# `requested_id` is what disambiguates same-path siblings.
# Two cards can share one weights path and differ only in
# settings -- Wan2.2-I2V-A14B-Lightning and the plain
# Wan2.2-I2V-A14B both point at
# Wan-AI/Wan2.2-I2V-A14B-Diffusers, declaring 22 GB and
# 151 GB. Without the id the lookup lands on whichever
# sibling registered the path, so the Lightning variant was
# sized at 151 GB, read as ~127 GB over free VRAM, and sent
# straight to CPU offload -- it then computed on the CPU at
# 0% GPU for 26 minutes. The manager has carried this
# disambiguation since the lisa/lisa-32k case; this caller
# simply never passed the id for it to use.
_base_gb = multi_model_manager._get_model_used_vram_gb(
model_key, model_name)
model_key, model_name,
requested_id=getattr(request, 'model', None))
_need_gb = _base_gb + _lora_extra_gb
_BORDERLINE_GB = 3.0 # how far over free VRAM still counts as "borderline"
_over_gb = _need_gb - _free_gb
......
......@@ -105,12 +105,26 @@ def _load_in_progress() -> bool:
A load streams multi-GB of weights into host RAM over many seconds, so RSS
climbs monotonically — which the leak heuristic would otherwise mistake for a
leak. The scheduler shows no active *request* lease during a load, so the
idle check alone doesn't catch this; we consult the manager's load event."""
idle check alone doesn't catch this; we consult the manager's load event.
TWO signals, because the event alone misses most of them. The ready event is
managed by the manager's own load_model path, and diffusers image/video
pipelines do not go through it — they load in codai/api/{images,video}.py.
So a 12-minute multi-shard load of a quantized video model looked idle, its
monotonically rising RSS looked exactly like a leak, and the watcher ran its
mitigation ladder against a load that was doing nothing wrong (measured on
Wan2.2-I2V-A14B-Lightning: "RSS 19.8/43.0 GB LEAK SUSPECTED" every poll).
The loading RESERVATIONS cover those paths: anything that reserved VRAM for
a load window is, by definition, loading."""
try:
from codai.models.manager import multi_model_manager
return not multi_model_manager._model_ready_event.is_set()
if not multi_model_manager._model_ready_event.is_set():
return True
if getattr(multi_model_manager, "_loading_reservations", None):
return True
except Exception:
return False
return False
def _process_ram_gb() -> float:
......
# CoderAI - OpenAI-compatible API server
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Two bugs with the same shape: a signal that existed and was not consulted.
1. Same-path siblings. Two cards can share one weights path and differ only in
settings -- Wan2.2-I2V-A14B-Lightning and the plain Wan2.2-I2V-A14B both
point at Wan-AI/Wan2.2-I2V-A14B-Diffusers, declaring 22 GB and 151 GB. The
manager has disambiguated these since the lisa/lisa-32k case, via
`requested_id`. The placement callers never passed it, so the Lightning
variant was sized at 151 GB, read as ~127 GB over free VRAM, and sent
straight to CPU offload -- it then computed on the CPU at 0% GPU.
2. The leak watcher's load signal. It consulted only the manager's ready event,
which diffusers image/video loads never touch, so a 12-minute multi-shard
load looked idle and its rising RSS looked like a leak.
"""
import inspect
import re
import pytest
# ------------------------------------------------- sizing the right sibling
def test_the_video_placement_passes_the_requested_id():
from codai.api import video
src = inspect.getsource(video.video_generations) \
if hasattr(video, "video_generations") else inspect.getsource(video)
m = re.search(r"_get_model_used_vram_gb\(\s*model_key,\s*model_name,\s*"
r"requested_id=", inspect.getsource(video))
assert m, ("the video placement decision sizes the model without "
"requested_id, so same-path siblings cannot be told apart")
def test_the_image_placement_passes_the_requested_id():
from codai.api import images
assert re.search(r"_get_model_used_vram_gb\([^)]*requested_id=",
inspect.getsource(images), re.S), (
"the image placement decision sizes the model without requested_id")
def test_the_manager_prefers_the_requested_cards_own_size():
"""The disambiguation itself: an alias that matches the requested id wins."""
from codai.models.manager import multi_model_manager as m
saved = dict(m.config or {})
try:
m.config.clear()
m.config.update({
"Wan-AI/Wan2.2-I2V-A14B-Diffusers": {
"path": "Wan-AI/Wan2.2-I2V-A14B-Diffusers",
"used_vram_gb": 151.42, "alias": None},
"__lightning": {
"path": "Wan-AI/Wan2.2-I2V-A14B-Diffusers",
"used_vram_gb": 22.0, "alias": "Wan2.2-I2V-A14B-Lightning"},
})
big = m._get_model_used_vram_gb("Wan-AI/Wan2.2-I2V-A14B-Diffusers")
small = m._get_model_used_vram_gb(
"Wan-AI/Wan2.2-I2V-A14B-Diffusers",
requested_id="Wan2.2-I2V-A14B-Lightning")
# Not the declared figures: the estimator scales them by a quantization
# factor. What matters is which CARD was consulted, and that the answer
# is small enough to stay on a 24 GiB device instead of being exiled.
assert small < big / 2, (
f"requested_id made no difference ({small} vs {big}) -- the "
f"Lightning card is still sized by its 151 GB sibling")
assert small < 24.0, (
f"the Lightning card is sized at {small} GB, which still does not "
f"fit a 24 GiB card, so it goes straight to CPU offload")
finally:
m.config.clear()
m.config.update(saved)
# --------------------------------------------- knowing a load is in progress
def test_a_loading_reservation_counts_as_a_load():
"""Diffusers image/video loads never touch the manager's ready event, so
the reservation is the only signal that covers them."""
from codai.models import ram_monitor
from codai.models.manager import multi_model_manager as m
saved = dict(m._loading_reservations)
try:
m._loading_reservations.clear()
m._model_ready_event.set()
assert ram_monitor._load_in_progress() is False
m._loading_reservations["some-model"] = 19.0
assert ram_monitor._load_in_progress() is True, (
"a reserved load window is not seen as loading, so its rising RSS "
"is mistaken for a leak")
finally:
m._loading_reservations.clear()
m._loading_reservations.update(saved)
def test_the_ready_event_still_counts():
from codai.models import ram_monitor
from codai.models.manager import multi_model_manager as m
saved = dict(m._loading_reservations)
try:
m._loading_reservations.clear()
m._model_ready_event.clear()
assert ram_monitor._load_in_progress() is True
finally:
m._model_ready_event.set()
m._loading_reservations.update(saved)
def test_a_leak_is_never_suspected_while_loading():
"""The heuristic resets its trend while loading; that only works if the
load is detected in the first place."""
from codai.models import ram_monitor
src = inspect.getsource(ram_monitor._loop)
assert "loading = _load_in_progress()" in src
assert "idle = _scheduler_idle() and not loading" in src
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