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, ...@@ -3285,8 +3285,13 @@ async def video_generations(request: VideoGenerationRequest,
f"free) — using full GPU (it falls back to offload on OOM)") f"free) — using full GPU (it falls back to offload on OOM)")
except Exception: except Exception:
pass 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() _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 from codai.tasks import loading_task
try: try:
with loading_task(model_name, model_type="video"): with loading_task(model_name, model_type="video"):
...@@ -3354,17 +3359,17 @@ async def video_generations(request: VideoGenerationRequest, ...@@ -3354,17 +3359,17 @@ async def video_generations(request: VideoGenerationRequest,
print(f" [video][accel] skipped: {_e}") print(f" [video][accel] skipped: {_e}")
multi_model_manager.models[model_key] = pipe multi_model_manager.models[model_key] = pipe
multi_model_manager.current_model_key = model_key multi_model_manager.current_model_key = model_key
# Record the real VRAM used. record_vram_delta only persists when no # Record the model's FULL footprint (GPU-resident + offloaded-to-RAM). Pass
# used_vram_gb is configured (it writes the separate measured_vram_gb). # ram_before so an offloaded load's host-RAM weights are counted — otherwise
# Under ANY offload strategy the weights live on CPU/disk, so the GPU # measured_vram_gb collapses to the tiny GPU slice (~0.3 GB) and eviction
# delta is a meaningless ~0 — never let it overwrite a real full-GPU # never frees room for the forward (an idle image model stays resident and
# measurement (that bug saved measured_vram_gb=0.05 and made the next # the forward OOMs).
# start mis-pick full-GPU and OOM-cascade).
try: try:
_strat = str(getattr(pipe, '_coderai_load_strategy', '') or '') _strat = str(getattr(pipe, '_coderai_load_strategy', '') or '')
_was_offloaded = bool(_strat) and not _strat.startswith('full GPU') _was_offloaded = bool(_strat) and not _strat.startswith('full GPU')
multi_model_manager.record_vram_delta( 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: except Exception:
pass pass
......
...@@ -2465,15 +2465,7 @@ class MultiModelManager: ...@@ -2465,15 +2465,7 @@ class MultiModelManager:
force_update = bool(cfgd.get("force_vram_update")) force_update = bool(cfgd.get("force_vram_update"))
user_pinned = cfgd.get("used_vram_gb") is not None user_pinned = cfgd.get("used_vram_gb") is not None
# --- GPU VRAM footprint (weights resident on GPU + runtime reserve) ------ # --- Host-RAM footprint (the portion of weights offloaded to CPU) -------
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) ----------------------
measured_ram = None measured_ram = None
if ram_before is not None and ram_before >= 0: if ram_before is not None and ram_before >= 0:
try: try:
...@@ -2483,6 +2475,23 @@ class MultiModelManager: ...@@ -2483,6 +2475,23 @@ class MultiModelManager:
except Exception: except Exception:
measured_ram = None 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) ------- # --- Settled GPU-layer split (the value the retry loop landed on) -------
measured_layers = None measured_layers = None
try: try:
...@@ -2491,12 +2500,14 @@ class MultiModelManager: ...@@ -2491,12 +2500,14 @@ class MultiModelManager:
except (TypeError, ValueError): except (TypeError, ValueError):
measured_layers = None 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}': " print(f"Measured footprint for '{model_key}': "
f"VRAM={'%.2f' % measured_vram if measured_vram is not None else 'n/a'} GB + " f"{'%.2f' % measured_vram if measured_vram is not None else 'n/a'} GB total "
f"RAM={'%.2f' % measured_ram if measured_ram is not None else 'n/a'} GB = " f"(full model footprint, placement-independent; of which "
f"{_total:.2f} GB total " f"{'%.2f' % measured_ram if measured_ram is not None else '0.00'} GB was "
f"(n_gpu_layers={measured_layers if measured_layers is not None else 'n/a'})") 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 # 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 # 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