173 lines
6.2 KiB
Python
173 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
import comfy.model_management as model_management
|
|
|
|
from .residency import REGISTRY
|
|
|
|
|
|
def _safe_free_memory(device) -> int:
|
|
return int(model_management.get_free_memory(device))
|
|
|
|
|
|
def _safe_is_dead(loaded) -> bool:
|
|
try:
|
|
return loaded.is_dead()
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _sort_key_for_candidate(entry, *, sticky_respected: bool) -> tuple[int, int, float]:
|
|
if not sticky_respected:
|
|
return (0, 0, getattr(entry, "last_touched", 0.0) if entry is not None else 0.0)
|
|
priority = int(getattr(entry, "priority", 0)) if entry is not None else 0
|
|
last_touched = float(getattr(entry, "last_touched", 0.0)) if entry is not None else 0.0
|
|
return (1, priority, last_touched)
|
|
|
|
|
|
def _trim_candidates(*, device, respect_sticky: bool, sticky_floor_priority: int) -> list[tuple[Any, Any, bool]]:
|
|
candidates: list[tuple[Any, Any, bool]] = []
|
|
for loaded in list(model_management.current_loaded_models):
|
|
if device is not None and loaded.device != device:
|
|
continue
|
|
if _safe_is_dead(loaded):
|
|
continue
|
|
|
|
model = getattr(loaded, "model", None)
|
|
if model is None:
|
|
continue
|
|
|
|
entry = REGISTRY.entry_for_object(model)
|
|
sticky_respected = (
|
|
respect_sticky
|
|
and entry is not None
|
|
and bool(getattr(entry, "sticky", False))
|
|
and int(getattr(entry, "priority", 0)) >= int(sticky_floor_priority)
|
|
)
|
|
candidates.append((loaded, entry, sticky_respected))
|
|
|
|
candidates.sort(key=lambda item: _sort_key_for_candidate(item[1], sticky_respected=item[2]))
|
|
return candidates
|
|
|
|
|
|
def trim_resident_vram(
|
|
*,
|
|
target_free_vram_bytes: int,
|
|
respect_sticky: bool,
|
|
sticky_floor_priority: int,
|
|
allow_partial_unload: bool,
|
|
) -> dict[str, Any]:
|
|
cleanup_models_gc = getattr(model_management, "cleanup_models_gc", None)
|
|
if callable(cleanup_models_gc):
|
|
cleanup_models_gc()
|
|
|
|
REGISTRY.refresh_runtime_state()
|
|
device = model_management.get_torch_device()
|
|
free_before = _safe_free_memory(device)
|
|
actions: list[dict[str, Any]] = []
|
|
soft_empty_cache = getattr(model_management, "soft_empty_cache", None)
|
|
stopped_reason = "target_met"
|
|
|
|
while True:
|
|
free_now = _safe_free_memory(device)
|
|
need = int(target_free_vram_bytes) - free_now
|
|
if need <= 0:
|
|
stopped_reason = "target_met"
|
|
break
|
|
|
|
candidates = _trim_candidates(
|
|
device=device,
|
|
respect_sticky=respect_sticky,
|
|
sticky_floor_priority=sticky_floor_priority,
|
|
)
|
|
if not candidates:
|
|
stopped_reason = "no_candidates"
|
|
break
|
|
|
|
loaded, entry, sticky_respected = candidates[0]
|
|
model = loaded.model
|
|
loaded_before = int(loaded.model_loaded_memory())
|
|
action = {
|
|
"entry_id": getattr(entry, "entry_id", None),
|
|
"basename": None if entry is None else os.path.basename(getattr(entry, "source_path", "") or ""),
|
|
"tracked": entry is not None,
|
|
"sticky_respected": sticky_respected,
|
|
"priority": None if entry is None else int(getattr(entry, "priority", 0)),
|
|
"need_before_bytes": need,
|
|
"loaded_before_bytes": loaded_before,
|
|
"freed_pinned_ram_bytes": 0,
|
|
}
|
|
|
|
if allow_partial_unload and hasattr(model, "pinned_memory_size") and hasattr(model, "partially_unload_ram"):
|
|
try:
|
|
pinned_memory = int(model.pinned_memory_size())
|
|
if pinned_memory > 0:
|
|
pinned_budget = min(pinned_memory, max(need, 0))
|
|
model.partially_unload_ram(pinned_budget)
|
|
action["freed_pinned_ram_bytes"] = pinned_budget
|
|
except Exception as exc:
|
|
action["pinned_ram_warning"] = str(exc)
|
|
|
|
try:
|
|
fully_unloaded = loaded.model_unload(need if allow_partial_unload else None)
|
|
action["mode"] = "full_unload" if fully_unloaded else "partial_unload"
|
|
except Exception as exc:
|
|
if allow_partial_unload:
|
|
try:
|
|
fully_unloaded = loaded.model_unload(None)
|
|
action["mode"] = "full_unload_fallback"
|
|
action["partial_unload_warning"] = str(exc)
|
|
except Exception as fallback_exc:
|
|
action["mode"] = "error"
|
|
action["error"] = str(fallback_exc)
|
|
actions.append(action)
|
|
stopped_reason = "error"
|
|
break
|
|
else:
|
|
action["mode"] = "error"
|
|
action["error"] = str(exc)
|
|
actions.append(action)
|
|
stopped_reason = "error"
|
|
break
|
|
|
|
if fully_unloaded:
|
|
try:
|
|
model_management.current_loaded_models.remove(loaded)
|
|
except ValueError:
|
|
pass
|
|
|
|
if callable(soft_empty_cache):
|
|
soft_empty_cache()
|
|
|
|
REGISTRY.refresh_runtime_state()
|
|
free_after = _safe_free_memory(device)
|
|
action["loaded_after_bytes"] = 0 if fully_unloaded else int(loaded.model_loaded_memory())
|
|
action["freed_vram_bytes"] = max(0, free_after - free_now)
|
|
action["free_after_bytes"] = free_after
|
|
actions.append(action)
|
|
|
|
if action["freed_vram_bytes"] <= 0 and not fully_unloaded:
|
|
stopped_reason = "no_progress"
|
|
break
|
|
|
|
free_after = _safe_free_memory(device)
|
|
target_met = free_after >= int(target_free_vram_bytes)
|
|
if target_met:
|
|
stopped_reason = "target_met"
|
|
return {
|
|
"status": "met_target" if target_met else ("error" if stopped_reason == "error" else "partial"),
|
|
"stopped_reason": stopped_reason,
|
|
"target_met": target_met,
|
|
"device": str(device),
|
|
"target_free_vram_bytes": int(target_free_vram_bytes),
|
|
"free_before_bytes": free_before,
|
|
"free_after_bytes": free_after,
|
|
"freed_vram_bytes": max(0, free_after - free_before),
|
|
"respect_sticky": bool(respect_sticky),
|
|
"sticky_floor_priority": int(sticky_floor_priority),
|
|
"allow_partial_unload": bool(allow_partial_unload),
|
|
"actions": actions,
|
|
}
|