Files
xiaolibai-sys 164896ed78 bump v1.1.4: nvfp4 support, GPU LoRA fold, loader I/O pipeline, async D2H, unified logging
- nvfp4: _quantization_metadata -> comfy_quant synthesis, SlotEntry extra params (block_scale), dual-scale LoRA fold, orig_dtype fixup
- LoRA: plain-mode block LoRAs no longer silently dropped; folded on GPU after weights land
- loader: parallel positioned reads, pipelined group load, no per-tensor clone, header-only dtype probes
- block swap: read_block intra-block parallel + disk_workers node option; async D2H write-back on transfer stream; lazy evict_all skips wasted write-back
- arch: split models/loader.py out of wan_model.py; single utils/keys.py; LoRA folding consolidated in utils/lora.py; global utils/log.py ([BerniniR][Tag] format, BERNINI_LOG_LEVEL)
2026-07-21 14:07:05 +08:00

175 lines
5.7 KiB
Python

"""
VRAM management utilities for Bernini-R.
Lightweight helpers wrapping ComfyUI's model_management and PyTorch's CUDA APIs.
"""
from __future__ import annotations
import gc
import torch
import comfy.model_management as mm
from .tensor_ops import free_module_storage
from .log import get_logger as _get_logger
logger = _get_logger("VRAM")
def get_compute_device() -> torch.device:
"""Return ComfyUI's configured compute device."""
return mm.get_torch_device()
def soft_empty_cache() -> None:
"""Safe wrapper around ComfyUI's soft_empty_cache."""
try:
mm.soft_empty_cache()
except Exception:
if torch.cuda.is_available():
torch.cuda.empty_cache()
def _empty_workingset() -> None:
"""Trim the Windows process working set to return physical pages."""
import sys
if sys.platform != "win32":
return
try:
import ctypes
kernel32 = ctypes.windll.kernel32
psapi = ctypes.windll.psapi
psapi.EmptyWorkingSet.argtypes = [ctypes.c_void_p]
psapi.EmptyWorkingSet.restype = ctypes.c_bool
h = kernel32.GetCurrentProcess()
psapi.EmptyWorkingSet(h)
except Exception:
pass
def collect_garbage(aggressive: bool = False) -> None:
"""Force Python GC + CUDA stream sync + cache flush.
The ``synchronize()`` is load-bearing on Windows: a model's block-swap
unload may still have async copies in flight on a CUDA stream. Freeing /
re-allocating VRAM behind a running transfer stream is exactly what triggers
``STATUS_ACCESS_VIOLATION`` (0xC0000005) during the *next* model load — the
dual-expert HIGH->LOW switch in particular. Waiting for all streams to
finish before we reclaim memory makes the switch safe.
When *aggressive* is True (used during the HIGH->LOW model switch), also
trim the Windows working set so physical RAM held by PyTorch's CPU
allocator is returned to the OS before the next large allocation.
"""
gc.collect()
if torch.cuda.is_available():
try:
torch.cuda.synchronize()
except Exception:
pass
soft_empty_cache()
try:
torch._C._host_emptyCache()
except Exception:
pass
if aggressive:
_empty_workingset()
gc.collect()
soft_empty_cache()
def get_free_vram_mb() -> float:
"""Get free VRAM in MiB."""
if not torch.cuda.is_available():
return 0.0
try:
free, total = torch.cuda.mem_get_info()
return free / (1024 * 1024)
except Exception:
return 0.0
def log_system_ram(tag: str = "") -> None:
"""Log available host RAM (MiB) so we can tell a leak from a tight machine.
Used during the dual-expert HIGH->LOW switch: if available RAM is still
~one model short of what LOW needs after we release HIGH, the problem is
the machine's RAM size, not a reference leak.
"""
try:
import psutil
vm = psutil.virtual_memory()
label = f" [{tag}]" if tag else ""
logger.info(
"RAM%s: %.1f / %.1f GiB free (%.1f%% used)",
label, vm.available / (1024 ** 3), vm.total / (1024 ** 3),
vm.percent,
)
except Exception:
# psutil not available — fall back to ctypes for a rough number.
try:
import ctypes
kernel32 = ctypes.windll.kernel32
class _MEMORYSTATUSEX(ctypes.Structure):
_fields_ = [
("dwLength", ctypes.c_ulong),
("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong),
("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong),
("ullAvailVirtual", ctypes.c_ulonglong),
]
ms = _MEMORYSTATUSEX()
ms.dwLength = ctypes.sizeof(ms)
kernel32.GlobalMemoryStatusEx(ctypes.byref(ms))
avail = ms.ullAvailPhys / (1024 ** 3)
total = ms.ullTotalPhys / (1024 ** 3)
label = f" [{tag}]" if tag else ""
logger.info(
"RAM%s: %.1f / %.1f GiB free", label, avail, total)
except Exception:
pass
def release_model_ram(patcher: Any) -> None:
"""Immediately shrink a model's weight tensors to release host RAM.
Uses :func:`tensor_ops.free_module_storage` which replaces every
parameter/buffer with a zero-size tensor. This works correctly for
``QuantizedTensor`` (fp8) and any other ``torch.Tensor`` subclass
because the old object is dereferenced — no ``__torch_dispatch__``
interception possible.
"""
try:
model = getattr(patcher, "model", None)
if model is None:
return
for module in model.modules():
free_module_storage(module)
except Exception as e:
logger.warning("release_model_ram shrink failed: %s", e)
collect_garbage(aggressive=True)
def log_memory(tag: str = "") -> None:
"""Log current VRAM usage."""
if not torch.cuda.is_available():
return
try:
free, total = torch.cuda.mem_get_info()
used = (total - free) / (1024 * 1024 * 1024)
total_gb = total / (1024 * 1024 * 1024)
label = f" [{tag}]" if tag else ""
logger.info(f"VRAM{label}: {used:.2f} / {total_gb:.2f} GiB used")
except Exception:
pass
def inference_mode():
"""Context manager: torch.inference_mode() with fallback to no_grad."""
try:
return torch.inference_mode()
except AttributeError:
return torch.no_grad()