fix(offload): reclaim model memory across switches

Model switches kept most of the previous model resident, and the next
load could stall in kernel reclaim while the freed memory was still held.

- strip group offload hooks in disable_offload so the meta move at unload
  actually frees component weights; hook removal resolves wrapper
  components that carry hooks on the inner model
- flush the torch pinned host cache in torch_gc so freed streaming
  buffers return to the OS instead of staying cached in-process
- skip the pipe-level accelerator move for group-managed pipes: the
  offload engine already placed every component, and the move only
  dragged on-demand components to the accelerator for the trailing
  eviction to undo
This commit is contained in:
CalamitousFelicitousness
2026-08-08 06:36:01 +01:00
parent 37f005a5d2
commit 5a66d9eabe
3 changed files with 11 additions and 5 deletions
+2
View File
@@ -245,6 +245,8 @@ def torch_gc(force: bool = False, fast: bool = False, reason: str | None = None)
torch.xpu.empty_cache()
if hasattr(torch.xpu, "ipc_collect"):
torch.xpu.ipc_collect()
if torch.cuda.is_available() and hasattr(torch._C, '_host_emptyCache'): # pylint: disable=protected-access
torch._C._host_emptyCache() # pylint: disable=protected-access # freed pinned host blocks otherwise stay cached in-process across model switches
except Exception as e:
log.error(f'Torch GC: {e}')
else:
+4 -1
View File
@@ -265,7 +265,10 @@ def move_model(model, device=None, force=False):
if hasattr(model, 'device') and model.device == torch.device('meta'):
set_execution_device(model, device)
elif hasattr(model, 'to'):
model.to(device)
if device == devices.device and getattr(model, 'sdnext_ondemand_modules', None):
pass # the group engine already placed every component; a pipe-level move would only drag on-demand components to the accelerator for the trailing eviction to undo
else:
model.to(device)
if hasattr(model, "prior_pipe"):
model.prior_pipe.to(device)
if device == devices.device:
+5 -4
View File
@@ -45,6 +45,7 @@ def get_signature(cls):
def disable_offload(sd_model):
remove_group_offload(sd_model) # group hooks block the meta move at unload, keeping component weights alive for as long as any reference to the pipe survives
if not getattr(sd_model, 'has_accelerate', False):
return
for module_name in get_module_names(sd_model):
@@ -95,11 +96,11 @@ def group_offload_config(main: bool) -> dict:
}
def remove_group_offload_component(module):
def remove_group_offload_component(module) -> bool:
if getattr(module, 'sdnext_group_offload_sig', None) is None:
module = getattr(module, 'model', None) # wrapper components carry the hooks on the inner model
if module is None or getattr(module, 'sdnext_group_offload_sig', None) is None:
return
return False
from diffusers.hooks.group_offloading import _GROUP_OFFLOADING, _LAYER_EXECUTION_TRACKER, _LAZY_PREFETCH_GROUP_OFFLOADING
from diffusers.hooks.hooks import HookRegistry
registry = HookRegistry.check_if_exists_or_initialize(module)
@@ -107,14 +108,14 @@ def remove_group_offload_component(module):
registry.remove_hook(_LAYER_EXECUTION_TRACKER, recurse=True)
registry.remove_hook(_LAZY_PREFETCH_GROUP_OFFLOADING, recurse=True)
module.sdnext_group_offload_sig = None
return True
def remove_group_offload(sd_model):
removed = []
for module_name in get_module_names(sd_model):
module = getattr(sd_model, module_name, None)
if isinstance(module, torch.nn.Module) and getattr(module, 'sdnext_group_offload_sig', None) is not None:
remove_group_offload_component(module)
if isinstance(module, torch.nn.Module) and remove_group_offload_component(module):
removed.append(module_name)
for module_name in getattr(sd_model, 'sdnext_ondemand_modules', None) or []:
module = getattr(sd_model, module_name, None)