Skip to content

Commit cae82a7

Browse files
yiyixuxuclaude
andauthored
support group offloading under auto offloading (#14358)
Let group offloaded models take part in auto offloading `ComponentsManager.enable_auto_cpu_offload` and `enable_group_offload` are two independent hook systems — accelerate's `_hf_hook` and diffusers' `HookRegistry` — and neither noticed the other. Enabling both raised nothing and appeared to work, but auto offloading frees memory by calling `.to()`, which a group offloaded module refuses and only warns about. Every offload the manager thought it performed was a no-op: it recorded memory as freed that never was, and it charged a group offloaded model's whole weight against the device although only one group is ever resident. A group offloaded model now takes part but places itself. It still makes room by moving other models aside, since its `pre_forward` consults the strategy as before; it is never chosen as the thing to move, because moving it does nothing; and the manager no longer pretends to offload it. Either order works, group offload before or after enabling. Deciding *what* to move then has to come from somewhere other than memory estimates, so `enable_auto_cpu_offload` takes an `offload_strategy` and `set_offload_strategy` can replace it later. The default `AutoOffloadStrategy` sizes its decisions from model memory footprints, which do not describe a model holding one group at a time, so it warns when it meets group offloading — only when no strategy was passed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4b8e466 commit cae82a7

1 file changed

Lines changed: 58 additions & 4 deletions

File tree

src/diffusers/modular_pipelines/components_manager.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import torch
2424

2525
from ..hooks import ModelHook
26+
from ..hooks.group_offloading import _is_group_offload_enabled
2627
from ..utils import (
2728
is_accelerate_available,
2829
logging,
@@ -76,12 +77,20 @@ def add_other_hook(self, hook: "UserCustomOffloadHook"):
7677
self.other_hooks.append(hook)
7778

7879
def init_hook(self, module):
80+
# A group offloaded module holds one group at a time and refuses `.to()`. Moving it here would be a
81+
# silent no-op that leaves this hook recording an offload that never happened.
82+
if _is_group_offload_enabled(module):
83+
return module
7984
return module.to("cpu")
8085

8186
def pre_forward(self, module, *args, **kwargs):
8287
if module.device != self.execution_device:
8388
if self.other_hooks is not None:
84-
hooks_to_offload = [hook for hook in self.other_hooks if hook.model.device == self.execution_device]
89+
hooks_to_offload = [
90+
hook
91+
for hook in self.other_hooks
92+
if hook.model.device == self.execution_device and not _is_group_offload_enabled(hook.model)
93+
]
8594
# offload all other hooks
8695
start_time = time.perf_counter()
8796
if self.offload_strategy is not None:
@@ -104,7 +113,10 @@ def pre_forward(self, module, *args, **kwargs):
104113

105114
if hooks_to_offload:
106115
clear_device_cache()
107-
module.to(self.execution_device)
116+
# The strategy still runs above, so a group offloaded model can make room for itself by moving other
117+
# models — it just places itself.
118+
if not _is_group_offload_enabled(module):
119+
module.to(self.execution_device)
108120
return send_to_device(args, self.execution_device), send_to_device(kwargs, self.execution_device)
109121

110122

@@ -336,6 +348,7 @@ def __init__(self):
336348
self.collections = OrderedDict() # collection_name -> set of component_names
337349
self.model_hooks = None
338350
self._auto_offload_enabled = False
351+
self._offload_strategy = None
339352

340353
def _lookup_ids(
341354
self,
@@ -692,7 +705,12 @@ def matches_pattern(component_id, pattern, exact_match=False):
692705

693706
return get_return_dict(matches, return_dict_with_names)
694707

695-
def enable_auto_cpu_offload(self, device: str | int | torch.device = None, memory_reserve_margin="3GB"):
708+
def enable_auto_cpu_offload(
709+
self,
710+
device: str | int | torch.device = None,
711+
memory_reserve_margin="3GB",
712+
offload_strategy=None,
713+
):
696714
"""
697715
Enable automatic CPU offloading for all components.
698716
@@ -703,11 +721,19 @@ def enable_auto_cpu_offload(self, device: str | int | torch.device = None, memor
703721
4. The system tries to offload the smallest combination of models that frees enough memory
704722
5. Models stay on the execution device until another model needs memory and forces them off
705723
724+
A group offloaded model takes part in this but places itself: it can still make room by moving other models
725+
aside, and is never moved to make room for them. Either order works — group offload before or after enabling
726+
this. `AutoOffloadStrategy` sizes its decisions from model memory footprints, which do not describe a model
727+
holding one group at a time, so pass an `offload_strategy` that decides from the workflow instead.
728+
706729
Args:
707730
device (str | int | torch.device): The execution device where models are moved for forward passes
708731
memory_reserve_margin (str): The memory reserve margin to use, default is 3GB. This is the amount of
709732
memory to keep free on the device to avoid running out of memory during model
710733
execution (e.g., for intermediate activations, gradients, etc.)
734+
offload_strategy: Any callable with the signature `(hooks, model_id, model, execution_device) -> hooks`,
735+
returning which resident models to offload before the incoming one loads. Defaults to
736+
`AutoOffloadStrategy`, which frees the smallest sufficient combination.
711737
"""
712738
if not is_accelerate_available():
713739
raise ImportError("Make sure to install accelerate to use auto_cpu_offload")
@@ -732,7 +758,17 @@ def enable_auto_cpu_offload(self, device: str | int | torch.device = None, memor
732758
remove_hook_from_module(component, recurse=True)
733759

734760
self.disable_auto_cpu_offload()
735-
offload_strategy = AutoOffloadStrategy(memory_reserve_margin=memory_reserve_margin)
761+
if offload_strategy is None:
762+
offload_strategy = AutoOffloadStrategy(memory_reserve_margin=memory_reserve_margin)
763+
if any(
764+
isinstance(component, torch.nn.Module) and _is_group_offload_enabled(component)
765+
for component in self.components.values()
766+
):
767+
logger.warning(
768+
"`AutoOffloadStrategy` decides what to move from model memory footprints, which do not "
769+
"describe a group offloaded model: it holds one group at a time, not its whole weight. Pass "
770+
"an `offload_strategy` that decides from the workflow instead."
771+
)
736772

737773
all_hooks = []
738774
for name, component in self.components.items():
@@ -749,6 +785,23 @@ def enable_auto_cpu_offload(self, device: str | int | torch.device = None, memor
749785
self.model_hooks = all_hooks
750786
self._auto_offload_enabled = True
751787
self._auto_offload_device = device
788+
self._offload_strategy = offload_strategy
789+
790+
def set_offload_strategy(self, offload_strategy):
791+
"""
792+
Replace the offload strategy on all managed models. Only valid while auto CPU offloading is enabled.
793+
794+
Args:
795+
offload_strategy:
796+
Any callable with the signature `(hooks, model_id, model, execution_device) -> hooks`: it receives the
797+
hooks of the models currently on the device and returns the ones to offload before the incoming model
798+
loads. The default is `AutoOffloadStrategy`, which frees the smallest sufficient combination.
799+
"""
800+
if not self._auto_offload_enabled:
801+
raise ValueError("Auto CPU offloading is not enabled. Call `enable_auto_cpu_offload` first.")
802+
for user_hook in self.model_hooks:
803+
user_hook.hook.offload_strategy = offload_strategy
804+
self._offload_strategy = offload_strategy
752805

753806
def disable_auto_cpu_offload(self):
754807
"""
@@ -765,6 +818,7 @@ def disable_auto_cpu_offload(self):
765818
clear_device_cache()
766819
self.model_hooks = None
767820
self._auto_offload_enabled = False
821+
self._offload_strategy = None
768822

769823
def get_model_info(
770824
self,

0 commit comments

Comments
 (0)