From 7215fc8e3466a9129ddd340d7a31abce75954276 Mon Sep 17 00:00:00 2001 From: liminfei-amd <91481003+liminfei-amd@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:28:50 +0800 Subject: [PATCH 1/2] model_management: tolerate cleanup_models finalizer re-entrancy in free_memory free_memory iterates current_loaded_models while a weakref finalizer (cleanup_models) can fire during garbage collection on the same thread and pop entries, leaving the cached absolute index stale (IndexError at the is_dynamic check, single-threaded). Iterate a snapshot, carry the LoadedModel object instead of the index, and remove unloaded entries by identity so a re-entrant pop is tolerated. Signed-off-by: liminfei-amd <91481003+liminfei-amd@users.noreply.github.com> --- comfy/model_management.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index b15d08ba1..c33388199 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -808,30 +808,37 @@ def free_memory(memory_required, device, keep_loaded=[], for_dynamic=False, pins can_unload = [] unloaded_models = [] - for i in range(len(current_loaded_models) -1, -1, -1): - shift_model = current_loaded_models[i] + # Iterate a snapshot: free_memory can re-enter itself on this thread when a + # weakref finalizer (cleanup_models) fires during GC mid-loop and pops from + # current_loaded_models. Carry the LoadedModel object (not a stale absolute + # index) and remove unloaded entries by identity so a concurrent pop is tolerated. + for shift_model in list(current_loaded_models): if device is None or shift_model.device == device: if shift_model not in keep_loaded and not shift_model.is_dead(): - can_unload.append((-shift_model.model_offloaded_memory(), sys.getrefcount(shift_model.model), shift_model.model_memory(), i)) + can_unload.append((-shift_model.model_offloaded_memory(), sys.getrefcount(shift_model.model), shift_model.model_memory(), shift_model)) shift_model.currently_used = False - can_unload_sorted = sorted(can_unload) + can_unload_sorted = sorted(can_unload, key=lambda a: a[:3]) for x in can_unload_sorted: - i = x[-1] + shift_model = x[-1] memory_to_free = 1e32 if not DISABLE_SMART_MEMORY or device is None: memory_to_free = 0 if device is None else memory_required - get_free_memory(device) - if current_loaded_models[i].model.is_dynamic() and for_dynamic: + if shift_model.model.is_dynamic() and for_dynamic: #don't actually unload dynamic models for the sake of other dynamic models #as that works on-demand. - memory_required -= current_loaded_models[i].model.loaded_size() + memory_required -= shift_model.model.loaded_size() memory_to_free = 0 - if memory_to_free > 0 and current_loaded_models[i].model_unload(memory_to_free): - logging.debug(f"Unloading {current_loaded_models[i].model.model.__class__.__name__}") - unloaded_model.append(i) + if memory_to_free > 0 and shift_model.model_unload(memory_to_free): + logging.debug(f"Unloading {shift_model.model.model.__class__.__name__}") + unloaded_model.append(shift_model) - for i in sorted(unloaded_model, reverse=True): - unloaded_models.append(current_loaded_models.pop(i)) + for shift_model in unloaded_model: + unloaded_models.append(shift_model) + for _idx in range(len(current_loaded_models) - 1, -1, -1): + if current_loaded_models[_idx] is shift_model: + current_loaded_models.pop(_idx) + break if not for_dynamic and pins_required > 0: ensure_pin_budget(pins_required) From 3711b4d01f7f79d0fd50019c04d157f66bd16953 Mon Sep 17 00:00:00 2001 From: liminfei-amd <91481003+liminfei-amd@users.noreply.github.com> Date: Mon, 29 Jun 2026 03:08:00 +0000 Subject: [PATCH 2/2] free_memory: drop explanatory comment per review Signed-off-by: liminfei-amd <91481003+liminfei-amd@users.noreply.github.com> --- comfy/model_management.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index c33388199..570084f86 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -808,10 +808,6 @@ def free_memory(memory_required, device, keep_loaded=[], for_dynamic=False, pins can_unload = [] unloaded_models = [] - # Iterate a snapshot: free_memory can re-enter itself on this thread when a - # weakref finalizer (cleanup_models) fires during GC mid-loop and pops from - # current_loaded_models. Carry the LoadedModel object (not a stale absolute - # index) and remove unloaded entries by identity so a concurrent pop is tolerated. for shift_model in list(current_loaded_models): if device is None or shift_model.device == device: if shift_model not in keep_loaded and not shift_model.is_dead():