From 2489f5c25be9d92acdc84fd306f760c30a9d29ee Mon Sep 17 00:00:00 2001 From: Booyaka101 Date: Sun, 10 May 2026 14:34:06 +0800 Subject: [PATCH 1/3] fix(nodes): restore non-quantized backups locally and skip super's mmap walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The override forwards unpatch_weights=True to comfy core's unpatch_model, which then calls self.model.to(device_to). Walking that .to() over the GGUF-quantized tensors that are still mmap-backed crashes with a Windows access violation (#444), surfacing whenever ComfyUI actually invokes unpatch_model — easiest repro is wrapping UnetLoaderGGUF in a node that returns multiple outputs, which flushes cache and triggers the unpatch path. The maintainer mitigated this in Sep 2024 (commit 6dbb4ba) by passing device_to=None to super, then reverted in 717a0e1 because that broke VRAM estimation. Reporter's proposed fix (passing unpatch_weights=False to super) avoids the crash but skips comfy core's non-quantized backup restoration — patch_weight_to_device's `if key not in self.backup` guard then never re-saves the original weight, and the next run's temp_weight = weight.to(...) reads the still-patched value, so LoRA deltas compound across runs. Restore the non-quantized backups in the override mirroring comfy core's loop (copy_to_param / set_attr_param + backup.clear() + comfy_patched_weights cleanup), then forward unpatch_weights=False so super skips its weight-restore + model.to(device_to) block. Keeps the crash fix without regressing LoRA correctness or VRAM accounting. Verified with a stub-based reproducer (verify_unpatch.py) covering three scenarios: pre-fix crashes on simulated mmap .to() — matches #444 reporter fix avoids crash, but non-quantized weight stays patched this fix avoids crash AND restores non-quantized backup correctly Closes #444 Co-Authored-By: Claude Opus 4.7 (1M context) --- nodes.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/nodes.py b/nodes.py index 4683514..33e0897 100644 --- a/nodes.py +++ b/nodes.py @@ -74,8 +74,29 @@ def unpatch_model(self, device_to=None, unpatch_weights=True): patches = getattr(p, "patches", []) if len(patches) > 0: p.patches = [] - # TODO: Find another way to not unload after patches - return super().unpatch_model(device_to=device_to, unpatch_weights=unpatch_weights) + # Restore non-quantized backups locally so we can short-circuit + # super()'s weight-unpatch block — its self.model.to(device_to) + # walk crashes with a Windows access violation on the still-mmap'd + # quantized tensors (#444). The maintainer's previous mitigation + # (Sep 2024 commit 6dbb4ba, reverted in 717a0e1) sidestepped the + # walk via device_to=None but skipped backup restoration, breaking + # LoRA correctness on subsequent runs (temp_weight in + # patch_weight_to_device reads the still-patched current weight). + # Doing the restore ourselves keeps non-quantized weights + # round-tripping correctly. + keys = list(self.backup.keys()) + for k in keys: + bk = self.backup[k] + if bk.inplace_update: + comfy.utils.copy_to_param(self.model, k, bk.weight) + else: + comfy.utils.set_attr_param(self.model, k, bk.weight) + self.model.current_weight_patches_uuid = None + self.backup.clear() + for m in self.model.modules(): + if hasattr(m, "comfy_patched_weights"): + del m.comfy_patched_weights + return super().unpatch_model(device_to=device_to, unpatch_weights=False) def pin_weight_to_device(self, key): From 32414a120ffb5c8256044e29ab33cbd007484845 Mon Sep 17 00:00:00 2001 From: Booyaka101 Date: Thu, 28 May 2026 01:24:23 +0800 Subject: [PATCH 2/3] nodes: also zero memory counters and run hook/pin cleanup in unpatch override The previous override forwarded unpatch_weights=False to super to dodge the self.model.to(device_to) walk that faults on mmap'd quantized tensors, but that also skipped the model_loaded_weight_memory / model_offload_buffer_memory reset super does in the same block. partially_load checks that counter immediately after calling unpatch_model and short-circuits self.load() if it is nonzero, so subsequent LoRA strength changes left .patches cleared without re-attaching anything (no LoRA applied at all on re-runs). Inline the full unpatch_weights body from base ModelPatcher minus that one device walk: hooks/pin/lowvram cleanup, backup restore, uuid reset, memory counters, comfy_patched_weights deletion. Upstream tracked at Comfy-Org/ComfyUI#14142. --- nodes.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/nodes.py b/nodes.py index 33e0897..225097a 100644 --- a/nodes.py +++ b/nodes.py @@ -67,25 +67,25 @@ def patch_weight_to_device(self, key, device_to=None, inplace_update=False): comfy.utils.set_attr_param(self.model, key, out_weight) def unpatch_model(self, device_to=None, unpatch_weights=True): + self.eject_model() if unpatch_weights: for p in self.model.parameters(): if is_torch_compatible(p): continue - patches = getattr(p, "patches", []) - if len(patches) > 0: + if len(getattr(p, "patches", [])) > 0: p.patches = [] - # Restore non-quantized backups locally so we can short-circuit - # super()'s weight-unpatch block — its self.model.to(device_to) - # walk crashes with a Windows access violation on the still-mmap'd - # quantized tensors (#444). The maintainer's previous mitigation - # (Sep 2024 commit 6dbb4ba, reverted in 717a0e1) sidestepped the - # walk via device_to=None but skipped backup restoration, breaking - # LoRA correctness on subsequent runs (temp_weight in - # patch_weight_to_device reads the still-patched current weight). - # Doing the restore ourselves keeps non-quantized weights - # round-tripping correctly. - keys = list(self.backup.keys()) - for k in keys: + # Mirror of base unpatch_model's unpatch_weights block, skipping the + # self.model.to(device_to) walk that faults on mmap'd quantized + # tensors (#444). Tracking upstream at Comfy-Org/ComfyUI#14142. + self.unpatch_hooks() + self.unpin_all_weights() + if self.model.model_lowvram: + for m in self.model.modules(): + comfy.model_patcher.move_weight_functions(m, device_to) + comfy.model_patcher.wipe_lowvram_weight(m) + self.model.model_lowvram = False + self.model.lowvram_patch_counter = 0 + for k in list(self.backup.keys()): bk = self.backup[k] if bk.inplace_update: comfy.utils.copy_to_param(self.model, k, bk.weight) @@ -93,6 +93,10 @@ def unpatch_model(self, device_to=None, unpatch_weights=True): comfy.utils.set_attr_param(self.model, k, bk.weight) self.model.current_weight_patches_uuid = None self.backup.clear() + if device_to is not None: + self.model.device = device_to + self.model.model_loaded_weight_memory = 0 + self.model.model_offload_buffer_memory = 0 for m in self.model.modules(): if hasattr(m, "comfy_patched_weights"): del m.comfy_patched_weights From b543b786d1eae558a0bb177b8042ac37dc9d5f58 Mon Sep 17 00:00:00 2001 From: Booyaka101 Date: Thu, 20 Aug 2026 13:23:58 +0800 Subject: [PATCH 3/3] nodes: replace mirrored unpatch block with explicit off-device weight move Skipping the base .to(device_to) walk kept a fully loaded model resident on CUDA after a real detach. unpatch_model zeroed the model_loaded_weight_memory and model_offload_buffer_memory counters while the quantized weights never left the device, so the next model saw a full GPU and got pushed into CPU offload. Caught by @woodenriver05 on the PR thread with a 19GB text encoder. Instead of mirroring base's unpatch_weights block minus the device walk, pass device_to=None so base skips only that walk, then move exactly the tensors that report a different device and reassign them through set_attr_param. Host-resident tensors are never touched, which keeps the mmap guarantee from #444: mapped memory is only ever read by a cross-device copy, never written in place and never walked by nn.Module.to. device_to=None to super is the same idea as 6dbb4ba, which 717a0e1 reverted for breaking VRAM estimation. The explicit move is the missing half that keeps the accounting correct. Measured on ComfyUI v0.33.1, torch 2.11.0+cu130, Windows 11, RTX 4090, with Wan2.2-I2V-A14B Q5_K_M (10427MB): full load then detach now frees CUDA back to 0MB (stayed at 10512MB before), free_memory after loading a second model frees both (stayed at 20475MB before), and LoRA strength changes 0.25 to 0.75 still reattach patches correctly, matching main byte for byte on the patch state. Co-Authored-By: Claude Fable 5 --- nodes.py | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/nodes.py b/nodes.py index 225097a..7b7a4a7 100644 --- a/nodes.py +++ b/nodes.py @@ -67,40 +67,31 @@ def patch_weight_to_device(self, key, device_to=None, inplace_update=False): comfy.utils.set_attr_param(self.model, key, out_weight) def unpatch_model(self, device_to=None, unpatch_weights=True): - self.eject_model() if unpatch_weights: for p in self.model.parameters(): if is_torch_compatible(p): continue - if len(getattr(p, "patches", [])) > 0: + patches = getattr(p, "patches", []) + if len(patches) > 0: p.patches = [] - # Mirror of base unpatch_model's unpatch_weights block, skipping the - # self.model.to(device_to) walk that faults on mmap'd quantized - # tensors (#444). Tracking upstream at Comfy-Org/ComfyUI#14142. - self.unpatch_hooks() - self.unpin_all_weights() - if self.model.model_lowvram: + if device_to is not None and self.model.model_lowvram: for m in self.model.modules(): comfy.model_patcher.move_weight_functions(m, device_to) - comfy.model_patcher.wipe_lowvram_weight(m) - self.model.model_lowvram = False - self.model.lowvram_patch_counter = 0 - for k in list(self.backup.keys()): - bk = self.backup[k] - if bk.inplace_update: - comfy.utils.copy_to_param(self.model, k, bk.weight) - else: - comfy.utils.set_attr_param(self.model, k, bk.weight) - self.model.current_weight_patches_uuid = None - self.backup.clear() - if device_to is not None: - self.model.device = device_to - self.model.model_loaded_weight_memory = 0 - self.model.model_offload_buffer_memory = 0 - for m in self.model.modules(): - if hasattr(m, "comfy_patched_weights"): - del m.comfy_patched_weights - return super().unpatch_model(device_to=device_to, unpatch_weights=False) + # device_to=None makes base skip its nn.Module.to walk, which touches + # still-mmap'd quantized tensors (#444). The walk below only moves + # tensors that live on another device, so host-resident (possibly + # mmap-backed) memory is read at most, never written or remapped. + # Tracking upstream at Comfy-Org/ComfyUI#14142. + super().unpatch_model(device_to=None, unpatch_weights=unpatch_weights) + if unpatch_weights and device_to is not None: + device_to = torch.device(device_to) + for key, param in list(self.model.named_parameters(remove_duplicate=False)): + if param.device != device_to: + comfy.utils.set_attr_param(self.model, key, param.to(device_to)) + for key, buf in list(self.model.named_buffers(remove_duplicate=False)): + if buf.device != device_to: + comfy.utils.set_attr(self.model, key, buf.to(device_to)) + self.model.device = device_to def pin_weight_to_device(self, key):