Skip to content

Commit 6cef71d

Browse files
authored
Fix group offloading with block_level and use_stream=True (#11375)
* fix * add tests * add message check
1 parent 026507c commit 6cef71d

2 files changed

Lines changed: 64 additions & 9 deletions

File tree

src/diffusers/hooks/group_offloading.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(
5757
non_blocking: bool = False,
5858
stream: Optional[torch.cuda.Stream] = None,
5959
record_stream: Optional[bool] = False,
60-
low_cpu_mem_usage=False,
60+
low_cpu_mem_usage: bool = False,
6161
onload_self: bool = True,
6262
) -> None:
6363
self.modules = modules
@@ -498,6 +498,8 @@ def _apply_group_offloading_block_level(
498498
option only matters when using streamed CPU offloading (i.e. `use_stream=True`). This can be useful when
499499
the CPU memory is a bottleneck but may counteract the benefits of using streams.
500500
"""
501+
if stream is not None and num_blocks_per_group != 1:
502+
raise ValueError(f"Using streams is only supported for num_blocks_per_group=1. Got {num_blocks_per_group=}.")
501503

502504
# Create module groups for ModuleList and Sequential blocks
503505
modules_with_group_offloading = set()
@@ -521,20 +523,16 @@ def _apply_group_offloading_block_level(
521523
stream=stream,
522524
record_stream=record_stream,
523525
low_cpu_mem_usage=low_cpu_mem_usage,
524-
onload_self=stream is None,
526+
onload_self=True,
525527
)
526528
matched_module_groups.append(group)
527529
for j in range(i, i + len(current_modules)):
528530
modules_with_group_offloading.add(f"{name}.{j}")
529531

530532
# Apply group offloading hooks to the module groups
531533
for i, group in enumerate(matched_module_groups):
532-
next_group = (
533-
matched_module_groups[i + 1] if i + 1 < len(matched_module_groups) and stream is not None else None
534-
)
535-
536534
for group_module in group.modules:
537-
_apply_group_offloading_hook(group_module, group, next_group)
535+
_apply_group_offloading_hook(group_module, group, None)
538536

539537
# Parameters and Buffers of the top-level module need to be offloaded/onloaded separately
540538
# when the forward pass of this module is called. This is because the top-level module is not
@@ -560,8 +558,10 @@ def _apply_group_offloading_block_level(
560558
record_stream=False,
561559
onload_self=True,
562560
)
563-
next_group = matched_module_groups[0] if len(matched_module_groups) > 0 else None
564-
_apply_group_offloading_hook(module, unmatched_group, next_group)
561+
if stream is None:
562+
_apply_group_offloading_hook(module, unmatched_group, None)
563+
else:
564+
_apply_lazy_group_offloading_hook(module, unmatched_group, None)
565565

566566

567567
def _apply_group_offloading_leaf_level(

tests/hooks/test_group_offloading.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import contextlib
1516
import gc
1617
import unittest
1718

@@ -20,6 +21,7 @@
2021
from diffusers.models import ModelMixin
2122
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
2223
from diffusers.utils import get_logger
24+
from diffusers.utils.import_utils import compare_versions
2325
from diffusers.utils.testing_utils import require_torch_gpu, torch_device
2426

2527

@@ -58,6 +60,39 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
5860
return x
5961

6062

63+
# This model implementation contains one type of block (single_blocks) instantiated before another type of block (double_blocks).
64+
# The invocation order of these blocks, however, is first the double_blocks and then the single_blocks.
65+
# With group offloading implementation before https://github.com/huggingface/diffusers/pull/11375, such a modeling implementation
66+
# would result in a device mismatch error because of the assumptions made by the code. The failure case occurs when using:
67+
# offload_type="block_level", num_blocks_per_group=2, use_stream=True
68+
# Post the linked PR, the implementation will work as expected.
69+
class DummyModelWithMultipleBlocks(ModelMixin):
70+
def __init__(
71+
self, in_features: int, hidden_features: int, out_features: int, num_layers: int, num_single_layers: int
72+
) -> None:
73+
super().__init__()
74+
75+
self.linear_1 = torch.nn.Linear(in_features, hidden_features)
76+
self.activation = torch.nn.ReLU()
77+
self.single_blocks = torch.nn.ModuleList(
78+
[DummyBlock(hidden_features, hidden_features, hidden_features) for _ in range(num_single_layers)]
79+
)
80+
self.double_blocks = torch.nn.ModuleList(
81+
[DummyBlock(hidden_features, hidden_features, hidden_features) for _ in range(num_layers)]
82+
)
83+
self.linear_2 = torch.nn.Linear(hidden_features, out_features)
84+
85+
def forward(self, x: torch.Tensor) -> torch.Tensor:
86+
x = self.linear_1(x)
87+
x = self.activation(x)
88+
for block in self.double_blocks:
89+
x = block(x)
90+
for block in self.single_blocks:
91+
x = block(x)
92+
x = self.linear_2(x)
93+
return x
94+
95+
6196
class DummyPipeline(DiffusionPipeline):
6297
model_cpu_offload_seq = "model"
6398

@@ -212,3 +247,23 @@ def test_error_raised_if_group_offloading_applied_on_sequential_offloaded_module
212247
pipe.enable_sequential_cpu_offload()
213248
with self.assertRaisesRegex(ValueError, "Cannot apply group offloading"):
214249
pipe.model.enable_group_offload(torch_device, offload_type="block_level", num_blocks_per_group=3)
250+
251+
def test_block_level_stream_with_invocation_order_different_from_initialization_order(self):
252+
if torch.device(torch_device).type != "cuda":
253+
return
254+
model = DummyModelWithMultipleBlocks(
255+
in_features=self.in_features,
256+
hidden_features=self.hidden_features,
257+
out_features=self.out_features,
258+
num_layers=self.num_layers,
259+
num_single_layers=self.num_layers + 1,
260+
)
261+
model.enable_group_offload(torch_device, offload_type="block_level", num_blocks_per_group=1, use_stream=True)
262+
263+
context = contextlib.nullcontext()
264+
if compare_versions("diffusers", "<=", "0.33.0"):
265+
# Will raise a device mismatch RuntimeError mentioning weights are on CPU but input is on device
266+
context = self.assertRaisesRegex(RuntimeError, "Expected all tensors to be on the same device")
267+
268+
with context:
269+
model(self.input)

0 commit comments

Comments
 (0)