Compare commits

...
Author SHA1 Message Date
xmarre fc7c0f6946 Tighten sticky VAE load preflight accounting 2026-04-16 07:32:50 +02:00
xmarre d56a716418 Budget sticky VAE preflight for model load 2026-04-16 07:25:31 +02:00
xmarre 27aa644775 Merge pull request #18 from xmarre/codex/inpaint-vae-node-fallback
Handle sticky tiled fallback for inpaint VAE encodes
2026-04-16 06:50:48 +02:00
xmarre e345e5f9d0 Avoid caching bound tiled VAE methods 2026-04-16 06:46:49 +02:00
coderabbitai[bot]andCodeRabbit 43fce69abd fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-04-16 04:37:55 +00:00
xmarre bafd34d48b Route sticky inpaint VAE encodes through tiled entrypoint 2026-04-16 06:30:29 +02:00
xmarre cf6843fd70 Merge pull request #17 from xmarre/codex/face-detailer-tiled-vae-admission
Wrap tiled VAE memory admission under sticky GPU
2026-04-16 05:41:26 +02:00
+193 -6
View File
@@ -686,6 +686,56 @@ def _sticky_vae_free_memory(*, device: Any, patcher: Any) -> int:
return max(0, int(patcher.get_free_memory(device)))
def _sticky_model_load_requirement(*, device: Any, models: tuple[Any, ...]) -> int | None:
import comfy.model_management as model_management
loaded_model_cls = getattr(model_management, "LoadedModel", None)
is_device_cpu = getattr(model_management, "is_device_cpu", None)
if device is None or loaded_model_cls is None:
return None
if callable(is_device_cpu) and is_device_cpu(device):
return 0
requested_models: set[Any] = set()
for model in models:
if model is None:
continue
requested_models.add(model)
additional_models = getattr(model, "model_patches_models", None)
if callable(additional_models):
try:
for additional in additional_models():
if additional is not None:
requested_models.add(additional)
except Exception as exc:
_LOG.debug(
"GPU Resident Loader: failed to enumerate patched models for sticky VAE preflight: %s",
exc,
)
return None
total_required = 0
for model in requested_models:
try:
loaded_model = loaded_model_cls(model)
loaded_device = getattr(loaded_model, "device", None)
if loaded_device != device:
continue
if callable(is_device_cpu) and is_device_cpu(loaded_device):
continue
total_required += max(0, int(loaded_model.model_memory_required(loaded_model.device)))
except Exception:
return None
return total_required
def _sticky_model_load_target(model_load_required: int | None) -> int | None:
if model_load_required is None:
return None
required = max(0, int(model_load_required))
return (required * 11 + 9) // 10
def _prepare_sticky_vae_batch(
*,
device: Any,
@@ -694,9 +744,12 @@ def _prepare_sticky_vae_batch(
total_batch_count: int,
) -> tuple[int, int, bool]:
free_memory = _sticky_vae_free_memory(device=device, patcher=patcher)
model_load_required = _sticky_model_load_requirement(device=device, models=(patcher,))
model_load_target = _sticky_model_load_target(model_load_required)
batch_budget = 0 if model_load_target is None else max(0, free_memory - model_load_target)
batch_number = _sticky_safe_batch_number(
batch_count=total_batch_count,
free_memory=free_memory,
free_memory=batch_budget,
memory_used=total_memory_used,
device=device,
)
@@ -705,7 +758,11 @@ def _prepare_sticky_vae_batch(
if REGISTRY.get_policy() != "sticky_gpu" or device is None:
return batch_number, batch_memory_used, False
target_free = _sticky_protection_target(batch_memory_used, device)
target_free = (
free_memory + 1
if model_load_target is None
else model_load_target + _sticky_protection_target(batch_memory_used, device)
)
if free_memory < target_free:
try:
trim_resident_vram(
@@ -717,25 +774,36 @@ def _prepare_sticky_vae_batch(
keep_models=(patcher,),
)
except Exception as exc:
_LOG.debug("GPU Resident Loader: proactive VAE trim failed for %s bytes: %s", batch_memory_used, exc)
_LOG.debug(
"GPU Resident Loader: proactive VAE trim failed for batch=%s load=%s bytes: %s",
batch_memory_used,
model_load_required,
exc,
)
free_memory = _sticky_vae_free_memory(device=device, patcher=patcher)
batch_budget = 0 if model_load_target is None else max(0, free_memory - model_load_target)
batch_number = _sticky_safe_batch_number(
batch_count=total_batch_count,
free_memory=free_memory,
free_memory=batch_budget,
memory_used=total_memory_used,
device=device,
)
batch_memory_used = _scaled_batch_memory(total_memory_used, total_batch_count, batch_number)
target_free = _sticky_protection_target(batch_memory_used, device)
target_free = (
free_memory + 1
if model_load_target is None
else model_load_target + _sticky_protection_target(batch_memory_used, device)
)
should_tile = free_memory < target_free and batch_number <= 1
if should_tile:
_LOG.info(
"GPU Resident Loader: skipping regular VAE pass and switching directly to tiled mode; free=%s target=%s batch_memory=%s",
"GPU Resident Loader: skipping regular VAE pass and switching directly to tiled mode; free=%s target=%s batch_memory=%s model_load=%s",
free_memory,
target_free,
batch_memory_used,
model_load_required,
)
return batch_number, batch_memory_used, should_tile
@@ -994,6 +1062,108 @@ def _call_tiled_vae(
return func(self, data, **kwargs)
def _should_prefer_tiled_vae_encode(vae: Any, pixel_samples: Any) -> bool:
if REGISTRY.get_policy() != "sticky_gpu":
return False
if vae is None or pixel_samples is None:
return False
try:
vae.throw_exception_if_invalid()
prepared = vae.vae_encode_crop_pixels(pixel_samples)
prepared = prepared.movedim(-1, 1)
if int(getattr(vae, "latent_dim", 2)) == 3 and prepared.ndim < 5:
if not getattr(vae, "not_video", False):
prepared = prepared.movedim(1, 0).unsqueeze(0)
else:
prepared = prepared.unsqueeze(2)
memory_used = vae.memory_used_encode(prepared.shape, vae.vae_dtype)
_, _, should_tile = _prepare_sticky_vae_batch(
device=getattr(vae, "device", None),
patcher=getattr(vae, "patcher", None),
total_memory_used=memory_used,
total_batch_count=prepared.shape[0],
)
return bool(should_tile)
except Exception as exc:
_LOG.debug("GPU Resident Loader: failed to preflight sticky VAE encode preference: %s", exc)
return False
def _call_bound_tiled_vae(func: Callable[..., Any], pixel_samples: Any, *args: Any, **kwargs: Any) -> Any:
supported_kwargs = _tiled_vae_supported_kwargs(getattr(func, "__func__", func))
filtered_kwargs = {key: value for key, value in kwargs.items() if key in supported_kwargs}
return func(pixel_samples, *args, **filtered_kwargs)
@contextlib.contextmanager
def _temporary_prefer_tiled_vae_encode(vae: Any):
original_encode = getattr(vae, "encode", None)
encode_tiled = getattr(vae, "encode_tiled", None)
if not callable(original_encode) or not callable(encode_tiled):
yield
return
had_instance_attr = "encode" in getattr(vae, "__dict__", {})
lock = getattr(vae, _TILED_VAE_MEMORY_LOCK_ATTR, None)
if lock is None:
with _TILED_VAE_LOCK_INIT:
lock = getattr(vae, _TILED_VAE_MEMORY_LOCK_ATTR, None)
if lock is None:
lock = threading.RLock()
setattr(vae, _TILED_VAE_MEMORY_LOCK_ATTR, lock)
def prefer_encode(pixel_samples, *args, **kwargs):
if _should_prefer_tiled_vae_encode(vae, pixel_samples):
return _call_bound_tiled_vae(encode_tiled, pixel_samples, *args, **kwargs)
return original_encode(pixel_samples, *args, **kwargs)
with lock:
setattr(vae, "encode", prefer_encode)
try:
yield
finally:
if had_instance_attr:
setattr(vae, "encode", original_encode)
else:
delattr(vae, "encode")
def _wrap_vae_encode_for_inpaint_node(func: Callable[..., Any]) -> Callable[..., Any]:
supported_kwargs = frozenset(inspect.signature(func).parameters)
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# Filter kwargs to only include those supported by the wrapped function
filtered_kwargs = {key: value for key, value in kwargs.items() if key in supported_kwargs}
# Supply default grow_mask_by if not present and supported
if "grow_mask_by" in supported_kwargs and "grow_mask_by" not in filtered_kwargs:
filtered_kwargs["grow_mask_by"] = 6
if REGISTRY.get_policy() != "sticky_gpu":
return func(self, *args, **filtered_kwargs)
# Extract vae from args for the context manager
vae = args[0] if args else kwargs.get("vae")
with _temporary_prefer_tiled_vae_encode(vae):
return func(self, *args, **filtered_kwargs)
return wrapper
def _wrap_inpaint_model_conditioning_node(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
def wrapper(self, positive, negative, pixels, vae, mask, noise_mask=True):
if REGISTRY.get_policy() != "sticky_gpu":
return func(self, positive, negative, pixels, vae, mask, noise_mask=noise_mask)
with _temporary_prefer_tiled_vae_encode(vae):
return func(self, positive, negative, pixels, vae, mask, noise_mask=noise_mask)
return wrapper
def _wrap_vae_encode_tiled(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
def wrapper(self, pixel_samples, tile_x=None, tile_y=None, overlap=None, tile_t=None, overlap_t=None):
@@ -1441,6 +1611,7 @@ def install_patches() -> None:
import comfy.model_patcher as model_patcher
import comfy.sd as comfy_sd
import comfy.utils as comfy_utils
import nodes as comfy_nodes
original_load_torch_file = _remember_original("utils.load_torch_file", comfy_utils.load_torch_file)
if comfy_utils.load_torch_file is original_load_torch_file:
@@ -1496,6 +1667,22 @@ def install_patches() -> None:
if comfy_sd.VAE.decode_tiled is original_vae_decode_tiled:
comfy_sd.VAE.decode_tiled = _wrap_vae_decode_tiled(original_vae_decode_tiled)
if hasattr(comfy_nodes, "VAEEncodeForInpaint") and hasattr(comfy_nodes.VAEEncodeForInpaint, "encode"):
original_vae_encode_for_inpaint = _remember_original(
"nodes.VAEEncodeForInpaint.encode",
comfy_nodes.VAEEncodeForInpaint.encode,
)
if comfy_nodes.VAEEncodeForInpaint.encode is original_vae_encode_for_inpaint:
comfy_nodes.VAEEncodeForInpaint.encode = _wrap_vae_encode_for_inpaint_node(original_vae_encode_for_inpaint)
if hasattr(comfy_nodes, "InpaintModelConditioning") and hasattr(comfy_nodes.InpaintModelConditioning, "encode"):
original_inpaint_model_conditioning = _remember_original(
"nodes.InpaintModelConditioning.encode",
comfy_nodes.InpaintModelConditioning.encode,
)
if comfy_nodes.InpaintModelConditioning.encode is original_inpaint_model_conditioning:
comfy_nodes.InpaintModelConditioning.encode = _wrap_inpaint_model_conditioning_node(original_inpaint_model_conditioning)
original_clip_vision_load = _remember_original("clip_vision.load", clip_vision.load)
if clip_vision.load is original_clip_vision_load:
clip_vision.load = _wrap_with_load_context(