Merge pull request #183 from pollockjj/issue-178

Fix Windows dynamic clip loading stack overflow (#178)
This commit is contained in:
John Pollock
2026-03-21 03:19:59 +00:00
committed by GitHub
3 changed files with 144 additions and 1 deletions
+139
View File
@@ -0,0 +1,139 @@
import logging
import comfy.model_management
import comfy.model_patcher
from comfy.model_patcher import QuantizedTensor, get_key_weight, low_vram_patch_estimate_vram
logger = logging.getLogger("MultiGPU")
_PATCH_MARKER = "_mgpu_issue21_clip_dynamic_load_list_guard"
_MODULE_THRESHOLD = 200
_DEPTH_THRESHOLD = 200
def _iter_named_modules_nonrecursive(module):
stack = [("", module)]
seen = set()
while stack:
prefix, current = stack.pop()
current_id = id(current)
if current_id in seen:
continue
seen.add(current_id)
yield prefix, current
children = list(current._modules.items())
for child_name, child in reversed(children):
if child is None:
continue
child_prefix = f"{prefix}.{child_name}" if prefix else child_name
stack.append((child_prefix, child))
def _iter_named_parameters_nonrecursive(module):
stack = [("", module)]
seen = set()
while stack:
prefix, current = stack.pop()
for name, param in current._parameters.items():
if param is None:
continue
param_id = id(param)
if param_id in seen:
continue
seen.add(param_id)
full_name = f"{prefix}.{name}" if prefix else name
yield full_name, param
children = list(current._modules.items())
for child_name, child in reversed(children):
if child is None:
continue
child_prefix = f"{prefix}.{child_name}" if prefix else child_name
stack.append((child_prefix, child))
def _graph_requires_guard(module):
stack = [(module, 0)]
seen = set()
module_count = 0
max_depth = 0
while stack:
current, depth = stack.pop()
current_id = id(current)
if current_id in seen:
continue
seen.add(current_id)
module_count += 1
max_depth = max(max_depth, depth)
if module_count > _MODULE_THRESHOLD or max_depth > _DEPTH_THRESHOLD:
return True
for child in current._modules.values():
if child is not None:
stack.append((child, depth + 1))
return False
def _safe_dynamic_load_list(self, default_device=None):
loading = []
for n, m in _iter_named_modules_nonrecursive(self.model):
default = False
params = dict(m.named_parameters(recurse=False))
if params:
for name, _ in _iter_named_parameters_nonrecursive(m):
if name not in params:
default = True
break
if default and default_device is not None:
for param_name, param in params.items():
param.data = param.data.to(
device=default_device,
dtype=getattr(m, param_name + "_comfy_model_dtype", None),
)
if not default and (hasattr(m, "comfy_cast_weights") or len(params) > 0):
module_mem = comfy.model_management.module_size(m)
module_offload_mem = module_mem
if hasattr(m, "comfy_cast_weights"):
def check_module_offload_mem(key):
if key in self.patches:
return low_vram_patch_estimate_vram(self.model, key)
model_dtype = getattr(self.model, "manual_cast_dtype", None)
weight, _, _ = get_key_weight(self.model, key)
if model_dtype is None or weight is None:
return 0
if weight.dtype != model_dtype or isinstance(weight, QuantizedTensor):
return weight.numel() * model_dtype.itemsize
return 0
module_offload_mem += check_module_offload_mem(f"{n}.weight")
module_offload_mem += check_module_offload_mem(f"{n}.bias")
sort_criteria = (module_offload_mem >= 64 * 1024, -module_offload_mem)
loading.append(sort_criteria + (module_mem, n, m, params))
return loading
def register_clip_dynamic_load_list_guard():
original = comfy.model_patcher.ModelPatcherDynamic._load_list
if getattr(original, _PATCH_MARKER, False):
return False
def guarded_load_list(self, for_dynamic=False, default_device=None):
if not for_dynamic:
return original(self, for_dynamic=for_dynamic, default_device=default_device)
if _graph_requires_guard(self.model):
logger.info("[MultiGPU Issue21] Using non-recursive ModelPatcherDynamic._load_list guard")
return _safe_dynamic_load_list(self, default_device=default_device)
return original(self, for_dynamic=for_dynamic, default_device=default_device)
setattr(guarded_load_list, _PATCH_MARKER, True)
comfy.model_patcher.ModelPatcherDynamic._load_list = guarded_load_list
logger.info("[MultiGPU Issue21] Registered ModelPatcherDynamic._load_list guard")
return True
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-multigpu"
description = "Provides a suite of custom nodes to manage multiple GPUs for ComfyUI, including advanced model offloading for both GGUF and Safetensor formats with DisTorch, and bespoke MultiGPU support for WanVideoWrapper and other custom nodes."
version = "2.6.1"
version = "2.6.2"
license = {file = "LICENSE"}
[project.urls]
+4
View File
@@ -522,6 +522,7 @@ def override_class_offload(cls):
def override_class_clip(cls):
"""Standard MultiGPU device override for CLIP models (with device kwarg workaround)"""
from . import set_current_text_encoder_device, get_current_text_encoder_device
from .clip_dynamic_load_list_guard import register_clip_dynamic_load_list_guard
class NodeOverride(cls):
@classmethod
@@ -540,6 +541,7 @@ def override_class_clip(cls):
original_text_device = get_current_text_encoder_device()
if device is not None:
set_current_text_encoder_device(device)
register_clip_dynamic_load_list_guard()
kwargs['device'] = 'default'
fn = getattr(super(), cls.FUNCTION)
out = fn(*args, **kwargs)
@@ -554,6 +556,7 @@ def override_class_clip(cls):
def override_class_clip_no_device(cls):
"""Standard MultiGPU device override for Triple/Quad CLIP models (no device kwarg workaround)"""
from . import set_current_text_encoder_device, get_current_text_encoder_device
from .clip_dynamic_load_list_guard import register_clip_dynamic_load_list_guard
class NodeOverride(cls):
@classmethod
@@ -572,6 +575,7 @@ def override_class_clip_no_device(cls):
original_text_device = get_current_text_encoder_device()
if device is not None:
set_current_text_encoder_device(device)
register_clip_dynamic_load_list_guard()
fn = getattr(super(), cls.FUNCTION)
out = fn(*args, **kwargs)
try: