manager: measured_vram_gb = FULL model footprint (GPU + offloaded), not the resident slice

Root cause of the persistent ~11 GB VRAM "base" and the big-clip OOMs: the video
model's persisted measured_vram_gb was 0.296 GB — the tiny GPU-resident slice of an
OFFLOADED load. With force_vram_update it overrode used_vram_gb=24.5, so
ensure_vram_for(video) thought the model needed ~0 GB and NEVER evicted the idle
Z-Image image model (~15 GB). That left ~11 GB resident; the video forward then
needed ~10 GB on top → 21 GB → OOM. (The whole GPU is coderai's — that base was
our own un-evicted model.)

Fix (per directive): measured_vram_gb now represents the FULL model footprint —
GPU-resident weights + the portion offloaded to host RAM + runtime reserve — i.e.
placement-independent total memory the model needs. record_vram_delta computes it
from the GPU delta PLUS the host-RAM delta (video path now passes ram_before), and
force_vram_update persists that full number every run. So eviction/strategy
provision for the real need and free room (evict the idle image model) for the
forward. Also cleared the stale 0.296 from models.json so it falls back to
used_vram_gb=24.5 until re-measured.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
parent b8bcdfea
......@@ -3285,8 +3285,13 @@ async def video_generations(request: VideoGenerationRequest,
f"free) — using full GPU (it falls back to offload on OOM)")
except Exception:
pass
# Snapshot free VRAM so we can record the real footprint after load.
# Snapshot free VRAM + own RAM so record_vram_delta can compute the FULL
# model footprint (GPU-resident weights + the portion offloaded to host RAM).
_vram_before = multi_model_manager.vram_before_load()
try:
_ram_before = multi_model_manager._get_own_ram_gb()
except Exception:
_ram_before = -1.0
from codai.tasks import loading_task
try:
with loading_task(model_name, model_type="video"):
......@@ -3354,17 +3359,17 @@ async def video_generations(request: VideoGenerationRequest,
print(f" [video][accel] skipped: {_e}")
multi_model_manager.models[model_key] = pipe
multi_model_manager.current_model_key = model_key
# Record the real VRAM used. record_vram_delta only persists when no
# used_vram_gb is configured (it writes the separate measured_vram_gb).
# Under ANY offload strategy the weights live on CPU/disk, so the GPU
# delta is a meaningless ~0 — never let it overwrite a real full-GPU
# measurement (that bug saved measured_vram_gb=0.05 and made the next
# start mis-pick full-GPU and OOM-cascade).
# Record the model's FULL footprint (GPU-resident + offloaded-to-RAM). Pass
# ram_before so an offloaded load's host-RAM weights are counted — otherwise
# measured_vram_gb collapses to the tiny GPU slice (~0.3 GB) and eviction
# never frees room for the forward (an idle image model stays resident and
# the forward OOMs).
try:
_strat = str(getattr(pipe, '_coderai_load_strategy', '') or '')
_was_offloaded = bool(_strat) and not _strat.startswith('full GPU')
multi_model_manager.record_vram_delta(
model_key, _vram_before, offloaded=_was_offloaded)
model_key, _vram_before, offloaded=_was_offloaded,
ram_before=_ram_before)
except Exception:
pass
......
......@@ -2465,15 +2465,7 @@ class MultiModelManager:
force_update = bool(cfgd.get("force_vram_update"))
user_pinned = cfgd.get("used_vram_gb") is not None
# --- GPU VRAM footprint (weights resident on GPU + runtime reserve) ------
delta_gb = (free_before - free_after) / 1e9
measured_vram = None
if delta_gb > 0:
reserve_gb = self._runtime_reserve_gb(cfgd, model_key, delta_gb)
measured_vram = round(delta_gb + reserve_gb, 3)
self._measured_vram_gb[model_key] = measured_vram # in-memory truth
# --- Host-RAM footprint (the CPU-offloaded layers) ----------------------
# --- Host-RAM footprint (the portion of weights offloaded to CPU) -------
measured_ram = None
if ram_before is not None and ram_before >= 0:
try:
......@@ -2483,6 +2475,23 @@ class MultiModelManager:
except Exception:
measured_ram = None
# --- FULL model footprint (what measured_vram_gb represents) ------------
# measured_vram_gb is the TOTAL memory the model needs — the GPU-resident
# weights PLUS the portion offloaded to host RAM PLUS the runtime/activation
# reserve. It is PLACEMENT-INDEPENDENT: offload just moves weights GPU<->CPU,
# the model still needs this much in total. Recording only the GPU-resident
# slice of an OFFLOADED load (~0.3 GB) made eviction think the model needed
# ~nothing, so it never freed room (e.g. an idle image model) and the forward
# then OOM'd. force_vram_update persists this full footprint every run.
delta_gb = (free_before - free_after) / 1e9
gpu_gb = max(0.0, delta_gb)
measured_vram = None
if gpu_gb > 0 or (measured_ram and measured_ram > 0):
reserve_gb = self._runtime_reserve_gb(
cfgd, model_key, gpu_gb + (measured_ram or 0.0))
measured_vram = round(gpu_gb + (measured_ram or 0.0) + reserve_gb, 3)
self._measured_vram_gb[model_key] = measured_vram # in-memory truth
# --- Settled GPU-layer split (the value the retry loop landed on) -------
measured_layers = None
try:
......@@ -2491,12 +2500,14 @@ class MultiModelManager:
except (TypeError, ValueError):
measured_layers = None
_total = (measured_vram or 0.0) + (measured_ram or 0.0)
# measured_vram already INCLUDES the offloaded RAM portion (full footprint),
# so don't add measured_ram again here — that would double-count.
print(f"Measured footprint for '{model_key}': "
f"VRAM={'%.2f' % measured_vram if measured_vram is not None else 'n/a'} GB + "
f"RAM={'%.2f' % measured_ram if measured_ram is not None else 'n/a'} GB = "
f"{_total:.2f} GB total "
f"(n_gpu_layers={measured_layers if measured_layers is not None else 'n/a'})")
f"{'%.2f' % measured_vram if measured_vram is not None else 'n/a'} GB total "
f"(full model footprint, placement-independent; of which "
f"{'%.2f' % measured_ram if measured_ram is not None else '0.00'} GB was "
f"offloaded to host RAM this load; "
f"n_gpu_layers={measured_layers if measured_layers is not None else 'n/a'})")
# Persist each system-owned field, accumulating into the in-memory config so
# successive fields don't clobber each other. Each is written to models.json
......
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