mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-23 09:09:01 +03:00
* tests: split block/leaf group offloading and derive the offloaded components `GroupOffloadTesterMixin.test_group_offloading_inference` ran both offload levels in one test body, so a pipeline that failed at one level had to skip both. Split it into `test_group_offloading_inference_block_level` and `test_group_offloading_inference_leaf_level`, sharing the helpers the test body used to define inline, and compare against the class-scoped `base_pipe_output` rather than rebuilding a baseline per level. The set of components to offload was a hardcoded list of eight names, so a pipeline with a component under any other name had it silently left on CPU for the forward pass to trip over. Derive the set instead: every `nn.Module` component is offloaded unless the config lists it in `group_offloading_leaf_level_exclude_modules`, the new `group_offloading_exclude_modules`, or `group_offloading_onload_component_names`. A name in either exclusion list that matches no component on the pipeline fails as the typo it is. Ideogram4's `unconditional_transformer` was one of the silently dropped components, which is why its group offload test was skipped; it now needs no declaration at all, and `text_encoder` picks up block-level coverage it never had. LTX2's skip goes the same way, with `audio_vae` declared alongside the other VAEs the tests keep on the accelerator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * style: fix `create_pipe` indentation in the group offload tester The method body was indented one level too deep, which `ruff format` rewrites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * tests: give group offloading one exclusion list per level `group_offloading_onload_component_names` named an implementation detail — components the tests keep resident — and justified itself with a mechanism that cannot occur: lazy prefetch mis-tracing a tiled VAE decode needs `use_stream=True`, which no test on this mixin sets, and most of the pipelines it covers never enable tiling either. What it actually encodes is a level-specific incapability, the mirror of `group_offloading_leaf_level_exclude_modules`. Leaf-level offloading onloads each leaf on its own `forward`, so it breaks on compute that reads a leaf's `.weight` directly. Block-level onloads a group when the group's leader runs its `forward`, so it breaks on compute that re-enters submodules without going through that leader — which is what a VAE decode path does. Rename it to `group_offloading_block_level_exclude_modules` and scope it to the level it describes. `group_offloading_exclude_modules` is then redundant, since a component that fails at both levels goes in both lists, and it never had a user. `vqvae` leaves the default: no pipeline in the new harness has one. Measured across the suite, offloading the `vae` at block level breaks 28 of 86 test classes, while all 86 offload it at leaf level with no failures -- coverage the component-scoped tests were skipping and the pipeline-level test has always had. Sweep before and after: the same 9 pre-existing failures, 782 -> 810 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Move group offloading changes to shared LTX-2 test mixins * tests: cover the group offloading stream prefetch path `use_stream=True` is the only way into `LazyPrefetchGroupOffloadingHook`, which traces the group execution order on the first forward pass, wires each group to its successor and flips `non_blocking` for the passes after. Nothing in the pipeline suite reached that code, which is how a stale comment about streams mis-tracing a tiled VAE decode survived unexamined for so long. Add `test_group_offloading_inference_block_level_streaming` and `..._leaf_level_streaming` alongside the synchronous pair, as separate methods rather than a parametrized test so a pipeline can skip one level without losing the other. Both route through `_run_group_offload_inference`, so they compare against the same `base_pipe_output` and honour the same per-level exclusion lists. `apply_group_offloading` raises rather than degrading when there is nowhere to put a stream, and `@require_torch_accelerator` still passes on MPS, so the two tests also check for CUDA or XPU before running. Guard against the flag quietly ceasing to have an effect: after enabling offloading with `use_stream=True`, assert at least one group across the pipeline actually carries a stream. The count is pipeline-wide because a component with no `ModuleList`/`Sequential` children has nothing to prefetch and legitimately streams nothing, and it asserts rather than skips because a regression that drops `use_stream` would zero every count and a skip would turn the whole streaming suite green by omission. Sweep over `tests/pipelines -k group_offloading`: 31 failed, 1145 passed, identical to the run without these tests. All 13 streaming failures have a non-streaming twin that fails the same way, so the prefetch path adds no failure of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * tests: port the group offload overrides orphaned by the level split Splitting `test_group_offloading_inference` into a block-level and a leaf-level test removed the name six other test files override, so their markers guarded a method the mixin no longer defines while the real tests ran unprotected. The `xfail(strict=True)` ones hid it: `super()` raised `AttributeError`, which counts as the expected failure, so they reported green while testing nothing. Rechecking each reason rather than renaming mechanically, three of the six turned out to describe behaviour that no longer exists: `LTX2DFRPipelineTesterConfig` subclasses `BasePipelineTesterConfig` rather than `LTX2BaseTesterConfig`, so the family's `audio_vae` block-level exclusion never reached it and its audio VAE was being offloaded. It needs the declaration, not a skip, and its group offload tests now pass. AudioLDM2 and Motif Video only fail at block level. `get_text_features()` and the vision tower bypass the `forward` that block-level offloading gates the group on, while leaf level onloads each leaf on its own `forward` and passes, so the skip and the xfail are scoped to block level and the leaf-level tests now run. The Kandinsky prior xfails stay at both levels but their reason was describing the hardcoded component list this branch removed. The actual cause is that the pipeline calls `PriorTransformer.post_process_latents()` after the denoising loop, reading `clip_mean` / `clip_std` that group offloading onloads only for the duration of `forward`. Also drop the class-count measurement from the block-level exclusion comment: it was already stale one merge later. The mechanism it was evidence for — `vae.decode()` never running `vae.forward()` — is the durable part. Sweep over `tests/pipelines -k group_offloading`: 31 failed -> 13, all of them failing identically on `main`, except `kolors`'s pipeline-level test, which fails in isolation on `main` too and only passes there because of the test order the split changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Clarify the Ideogram 4 block-level group offloading module exclusion comment * tests: parametrize `use_stream` instead of splitting the streaming tests The streaming tests were separate methods so a pipeline could skip or xfail them independently of their synchronous twins. Nothing has ever needed that: across three full sweeps every streaming failure had a synchronous twin at the same level and vice versa, and none of the sixteen downstream overrides marked a streaming variant differently from its twin. The two *levels* do get scoped apart — AudioLDM2 and Motif Video are block-level only — so those keep their own methods. Fold `use_stream` into a parameter on each level's test. Four mixin tests become two and sixteen override methods become eight, with the same coverage: a full sweep of the parametrized and the separate-method versions on the same tree gives 15 failed, 1263 passed, 152 skipped, 22 xfailed, 1 xpassed on both, with identical failure sets once `[stream]` and the `_streaming` suffix are normalised. Marks do not carry through a subclass override, so an override that forgets to re-declare the parametrize collapses to one un-parametrized test that errors on the missing argument — which an `xfail` then reports as green, the same failure mode the orphaned overrides had. `_USE_STREAM` carries a note saying so, and the six overrides re-declare it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * tests: point the PNDM marker comment at the split group offload tests `test_group_offloading_inference` no longer exists — it became one test per offload level — so the comment listing which tests share the class-level xfail named a test that is not there, and undercounted them by one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: record the group offload streaming parameter in the testing guide The shared memory mixin now runs each offload level with and without `use_stream`, which is the only path that reaches group offloading's lazy prefetch. Say so next to the exclusion-list guidance, along with the reason it is a parameter rather than a separate test, and the requirement that a subclass overriding either test re-declare the `parametrize` — marks do not carry through an override, and forgetting leaves a test that errors on the missing argument and is reported as green by an `xfail`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * tests: inline the single-caller group offload helpers `_split_group_offload_components` and `_count_streamed_groups` each had one caller, so per `code_style.md` they read better at the call site. The reasons they existed survive as comments: that a component is offloaded unless the level's list names it, and that several submodules share one `ModuleGroup` so the stream count has to dedupe on group identity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: condense the group offload guidance to one bullet The three group offload bullets in the testing guide had grown to explain the hook mechanism at a level of detail that answers a reviewer's questions rather than telling a test author what to write. Collapse them into one bullet keeping the parts that change what a PR does: which remedy to reach for, which exclusion list matches which hazard, that an unmatched name fails as a typo, and that an override must re-declare the `use_stream` parametrize. The worked examples below the bullet already carry the concrete diagnosis, and the parametrize trap is explained in full at the code site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Explicitly declares group offloading leaf level excluded components for LTX-2 * Simplify comment regarding _USE_STREAM in GroupOffloadTesterMixin --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
471 lines
22 KiB
Python
471 lines
22 KiB
Python
# coding=utf-8
|
|
# Copyright 2025 HuggingFace Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from diffusers import DiffusionPipeline
|
|
from diffusers.hooks import apply_group_offloading
|
|
|
|
from ...testing_utils import (
|
|
assert_tensors_close,
|
|
is_accelerate_available,
|
|
is_cpu_offload,
|
|
is_group_offload,
|
|
is_memory,
|
|
require_accelerate_version_greater,
|
|
require_accelerator,
|
|
require_torch_accelerator,
|
|
torch_device,
|
|
)
|
|
from .common import BasePipelineOutputMixin, BasePipelineTesterConfig, cast_pipeline_to_dtype
|
|
|
|
|
|
if is_accelerate_available():
|
|
import accelerate
|
|
|
|
|
|
@is_cpu_offload
|
|
class PipelineOffloadTesterMixin(BasePipelineOutputMixin):
|
|
"""CPU/sequential offload and accelerate `device_map` loading for pipelines."""
|
|
|
|
@require_accelerator
|
|
@require_accelerate_version_greater("0.14.0")
|
|
def test_sequential_cpu_offload_forward_pass(self, base_pipe_output, expected_max_diff=1e-4):
|
|
pipe = self.get_pipeline().to(torch_device)
|
|
|
|
pipe.enable_sequential_cpu_offload(device=torch_device)
|
|
assert pipe._execution_device.type == torch_device
|
|
|
|
inputs = self.get_dummy_inputs()
|
|
torch.manual_seed(0)
|
|
output_with_offload = pipe(**inputs)[0]
|
|
|
|
assert_tensors_close(
|
|
output_with_offload,
|
|
base_pipe_output,
|
|
atol=expected_max_diff,
|
|
msg="CPU offloading should not affect the inference results",
|
|
)
|
|
|
|
# make sure all `torch.nn.Module` components (except those in `self._exclude_from_cpu_offload`) are offloaded
|
|
offloaded_modules = {
|
|
k: v
|
|
for k, v in pipe.components.items()
|
|
if isinstance(v, torch.nn.Module) and k not in pipe._exclude_from_cpu_offload
|
|
}
|
|
# 1. all offloaded modules should be saved to cpu and moved to meta device
|
|
assert all(v.device.type == "meta" for v in offloaded_modules.values()), (
|
|
f"Not offloaded: {[k for k, v in offloaded_modules.items() if v.device.type != 'meta']}"
|
|
)
|
|
# 2. all offloaded modules should have hook installed
|
|
assert all(hasattr(v, "_hf_hook") for k, v in offloaded_modules.items()), (
|
|
f"No hook attached: {[k for k, v in offloaded_modules.items() if not hasattr(v, '_hf_hook')]}"
|
|
)
|
|
# 3. all offloaded modules should have correct hooks installed, should be either one of these two
|
|
# - `AlignDevicesHook`
|
|
# - a `SequentialHook` that contains `AlignDevicesHook`
|
|
offloaded_modules_with_incorrect_hooks = {}
|
|
for k, v in offloaded_modules.items():
|
|
if hasattr(v, "_hf_hook"):
|
|
if isinstance(v._hf_hook, accelerate.hooks.SequentialHook):
|
|
for hook in v._hf_hook.hooks:
|
|
if not isinstance(hook, accelerate.hooks.AlignDevicesHook):
|
|
offloaded_modules_with_incorrect_hooks[k] = type(v._hf_hook.hooks[0])
|
|
elif not isinstance(v._hf_hook, accelerate.hooks.AlignDevicesHook):
|
|
offloaded_modules_with_incorrect_hooks[k] = type(v._hf_hook)
|
|
|
|
assert len(offloaded_modules_with_incorrect_hooks) == 0, (
|
|
f"Not installed correct hook: {offloaded_modules_with_incorrect_hooks}"
|
|
)
|
|
|
|
@require_accelerator
|
|
@require_accelerate_version_greater("0.17.0")
|
|
def test_model_cpu_offload_forward_pass(self, base_pipe_output, expected_max_diff=2e-4):
|
|
pipe = self.get_pipeline().to(torch_device)
|
|
|
|
pipe.enable_model_cpu_offload(device=torch_device)
|
|
assert pipe._execution_device.type == torch_device
|
|
|
|
inputs = self.get_dummy_inputs()
|
|
torch.manual_seed(0)
|
|
output_with_offload = pipe(**inputs)[0]
|
|
|
|
assert_tensors_close(
|
|
output_with_offload,
|
|
base_pipe_output,
|
|
atol=expected_max_diff,
|
|
msg="CPU offloading should not affect the inference results",
|
|
)
|
|
|
|
offloaded_modules = {
|
|
k: v
|
|
for k, v in pipe.components.items()
|
|
if isinstance(v, torch.nn.Module) and k not in pipe._exclude_from_cpu_offload
|
|
}
|
|
# 1. check if all offloaded modules are saved to cpu
|
|
assert all(v.device.type == "cpu" for v in offloaded_modules.values()), (
|
|
f"Not offloaded: {[k for k, v in offloaded_modules.items() if v.device.type != 'cpu']}"
|
|
)
|
|
# 2. check if all offloaded modules have hooks installed
|
|
assert all(hasattr(v, "_hf_hook") for k, v in offloaded_modules.items()), (
|
|
f"No hook attached: {[k for k, v in offloaded_modules.items() if not hasattr(v, '_hf_hook')]}"
|
|
)
|
|
# 3. check if all offloaded modules have correct type of hooks installed, should be `CpuOffload`
|
|
offloaded_modules_with_incorrect_hooks = {}
|
|
for k, v in offloaded_modules.items():
|
|
if hasattr(v, "_hf_hook") and not isinstance(v._hf_hook, accelerate.hooks.CpuOffload):
|
|
offloaded_modules_with_incorrect_hooks[k] = type(v._hf_hook)
|
|
|
|
assert len(offloaded_modules_with_incorrect_hooks) == 0, (
|
|
f"Not installed correct hook: {offloaded_modules_with_incorrect_hooks}"
|
|
)
|
|
|
|
@require_accelerator
|
|
@require_accelerate_version_greater("0.17.0")
|
|
def test_cpu_offload_forward_pass_twice(self, expected_max_diff=2e-4):
|
|
pipe = self.get_pipeline()
|
|
|
|
pipe.enable_model_cpu_offload()
|
|
inputs = self.get_dummy_inputs()
|
|
output_with_offload = pipe(**inputs)[0]
|
|
|
|
pipe.enable_model_cpu_offload()
|
|
inputs = self.get_dummy_inputs()
|
|
output_with_offload_twice = pipe(**inputs)[0]
|
|
|
|
assert_tensors_close(
|
|
output_with_offload,
|
|
output_with_offload_twice,
|
|
atol=expected_max_diff,
|
|
msg="running CPU offloading 2nd time should not affect the inference results",
|
|
)
|
|
|
|
offloaded_modules = {
|
|
k: v
|
|
for k, v in pipe.components.items()
|
|
if isinstance(v, torch.nn.Module) and k not in pipe._exclude_from_cpu_offload
|
|
}
|
|
# 1. check if all offloaded modules are saved to cpu
|
|
assert all(v.device.type == "cpu" for v in offloaded_modules.values()), (
|
|
f"Not offloaded: {[k for k, v in offloaded_modules.items() if v.device.type != 'cpu']}"
|
|
)
|
|
# 2. check if all offloaded modules have hooks installed
|
|
assert all(hasattr(v, "_hf_hook") for k, v in offloaded_modules.items()), (
|
|
f"No hook attached: {[k for k, v in offloaded_modules.items() if not hasattr(v, '_hf_hook')]}"
|
|
)
|
|
# 3. check if all offloaded modules have correct type of hooks installed, should be `CpuOffload`
|
|
offloaded_modules_with_incorrect_hooks = {}
|
|
for k, v in offloaded_modules.items():
|
|
if hasattr(v, "_hf_hook") and not isinstance(v._hf_hook, accelerate.hooks.CpuOffload):
|
|
offloaded_modules_with_incorrect_hooks[k] = type(v._hf_hook)
|
|
|
|
assert len(offloaded_modules_with_incorrect_hooks) == 0, (
|
|
f"Not installed correct hook: {offloaded_modules_with_incorrect_hooks}"
|
|
)
|
|
|
|
@require_accelerator
|
|
@require_accelerate_version_greater("0.14.0")
|
|
def test_sequential_offload_forward_pass_twice(self, expected_max_diff=2e-4):
|
|
pipe = self.get_pipeline()
|
|
|
|
pipe.enable_sequential_cpu_offload(device=torch_device)
|
|
inputs = self.get_dummy_inputs()
|
|
output_with_offload = pipe(**inputs)[0]
|
|
|
|
pipe.enable_sequential_cpu_offload(device=torch_device)
|
|
inputs = self.get_dummy_inputs()
|
|
output_with_offload_twice = pipe(**inputs)[0]
|
|
|
|
assert_tensors_close(
|
|
output_with_offload,
|
|
output_with_offload_twice,
|
|
atol=expected_max_diff,
|
|
msg="running sequential offloading second time should have the inference results",
|
|
)
|
|
|
|
offloaded_modules = {
|
|
k: v
|
|
for k, v in pipe.components.items()
|
|
if isinstance(v, torch.nn.Module) and k not in pipe._exclude_from_cpu_offload
|
|
}
|
|
# 1. check if all offloaded modules are moved to meta device
|
|
assert all(v.device.type == "meta" for v in offloaded_modules.values()), (
|
|
f"Not offloaded: {[k for k, v in offloaded_modules.items() if v.device.type != 'meta']}"
|
|
)
|
|
# 2. check if all offloaded modules have hook installed
|
|
assert all(hasattr(v, "_hf_hook") for k, v in offloaded_modules.items()), (
|
|
f"No hook attached: {[k for k, v in offloaded_modules.items() if not hasattr(v, '_hf_hook')]}"
|
|
)
|
|
# 3. check if all offloaded modules have correct hooks installed, should be either one of these two
|
|
# - `AlignDevicesHook`
|
|
# - a `SequentialHook` that contains `AlignDevicesHook`
|
|
offloaded_modules_with_incorrect_hooks = {}
|
|
for k, v in offloaded_modules.items():
|
|
if hasattr(v, "_hf_hook"):
|
|
if isinstance(v._hf_hook, accelerate.hooks.SequentialHook):
|
|
for hook in v._hf_hook.hooks:
|
|
if not isinstance(hook, accelerate.hooks.AlignDevicesHook):
|
|
offloaded_modules_with_incorrect_hooks[k] = type(v._hf_hook.hooks[0])
|
|
elif not isinstance(v._hf_hook, accelerate.hooks.AlignDevicesHook):
|
|
offloaded_modules_with_incorrect_hooks[k] = type(v._hf_hook)
|
|
|
|
assert len(offloaded_modules_with_incorrect_hooks) == 0, (
|
|
f"Not installed correct hook: {offloaded_modules_with_incorrect_hooks}"
|
|
)
|
|
|
|
def test_pipeline_with_accelerator_device_map(self, tmp_path, base_pipe_output, expected_max_difference=1e-4):
|
|
pipe = self.get_pipeline().to(torch_device)
|
|
|
|
pipe.save_pretrained(tmp_path)
|
|
|
|
loaded_pipe = self.pipeline_class.from_pretrained(tmp_path, device_map=torch_device)
|
|
|
|
inputs = self.get_dummy_inputs()
|
|
torch.manual_seed(0)
|
|
loaded_out = loaded_pipe(**inputs)[0]
|
|
assert_tensors_close(
|
|
loaded_out, base_pipe_output, atol=expected_max_difference, msg="device_map loaded output changed."
|
|
)
|
|
|
|
|
|
class LayerwiseCastingTesterMixin(BasePipelineOutputMixin):
|
|
"""Layerwise FP8 casting during pipeline inference."""
|
|
|
|
def test_layerwise_casting_inference(self):
|
|
pipe = self.get_pipeline()
|
|
denoiser = getattr(pipe, "transformer", None) or getattr(pipe, "unet", None)
|
|
if denoiser is None or not hasattr(denoiser, "enable_layerwise_casting"):
|
|
pytest.skip(f"{self.pipeline_class.__name__} has no denoiser that supports layerwise casting.")
|
|
|
|
# Cast per component rather than with `.to(dtype=...)`: `enable_layerwise_casting` keeps
|
|
# `_keep_in_fp32_modules` submodules in float32 (it folds the declaration into its skip patterns), so
|
|
# casting them here would fail the forward pass before layerwise casting is exercised at all.
|
|
pipe.to(torch_device)
|
|
cast_pipeline_to_dtype(pipe, torch.bfloat16)
|
|
pipe.set_progress_bar_config(disable=None)
|
|
|
|
denoiser.enable_layerwise_casting(storage_dtype=torch.float8_e4m3fn, compute_dtype=torch.bfloat16)
|
|
|
|
inputs = self.get_dummy_inputs()
|
|
output = pipe(**inputs)[0]
|
|
|
|
assert not torch.isnan(output).any(), (
|
|
f"`{self.pipeline_class.__name__}` produced NaNs during layerwise casting inference."
|
|
)
|
|
|
|
|
|
@is_group_offload
|
|
class GroupOffloadTesterMixin(BasePipelineOutputMixin):
|
|
"""Block/leaf-level group offload, both component-scoped and pipeline-level orchestration."""
|
|
|
|
def create_pipe(self):
|
|
torch.manual_seed(0)
|
|
return self.get_pipeline()
|
|
|
|
def _skip_if_group_offloading_unsupported(self, pipe):
|
|
for component in pipe.components.values():
|
|
if hasattr(component, "_supports_group_offloading") and not component._supports_group_offloading:
|
|
pytest.skip(f"{self.pipeline_class.__name__} has a component that does not support group offloading.")
|
|
|
|
def _group_offload_exclude_modules(self, pipe, offload_type):
|
|
"""Config-declared components to keep out of group offloading at `offload_type`.
|
|
|
|
The two levels fail on opposite hazards, so each reads its own list — see the attributes on
|
|
`BasePipelineTesterConfig`. Every group offload test routes its exclusions through here, so a name a config
|
|
adds that matches no component on the pipeline is reported as the typo it is rather than silently costing
|
|
coverage and surfacing later as a device mismatch.
|
|
"""
|
|
if offload_type == "leaf_level":
|
|
exclude = set(self.group_offloading_leaf_level_exclude_modules)
|
|
elif offload_type == "block_level":
|
|
exclude = set(self.group_offloading_block_level_exclude_modules)
|
|
else:
|
|
raise ValueError(f"Unknown `offload_type` {offload_type!r}.")
|
|
|
|
# Only names a config adds are checked: the shared defaults cover the whole suite and most pipelines have
|
|
# just some of them. The check is against every registered component rather than the module-valued ones, so
|
|
# that excluding an optional component a config leaves unset reads as the no-op it is instead of a typo.
|
|
shared_defaults = set(BasePipelineTesterConfig.group_offloading_leaf_level_exclude_modules) | set(
|
|
BasePipelineTesterConfig.group_offloading_block_level_exclude_modules
|
|
)
|
|
unknown = sorted(exclude - set(pipe.components) - shared_defaults)
|
|
assert not unknown, (
|
|
f"{type(self).__name__} excludes {unknown} from {offload_type} group offloading, but "
|
|
f"{self.pipeline_class.__name__} has no such component. Its components are "
|
|
f"{sorted(pipe.components)}."
|
|
)
|
|
return exclude
|
|
|
|
def _enable_group_offload_on_components(self, pipe, **group_offloading_kwargs):
|
|
# Every module component is offloaded unless the list for this level names it, so a component a pipeline
|
|
# adds under a name this file has never heard of is covered by default rather than left on CPU.
|
|
module_names = [name for name, component in pipe.components.items() if isinstance(component, torch.nn.Module)]
|
|
onload_names = self._group_offload_exclude_modules(pipe, group_offloading_kwargs["offload_type"])
|
|
offload_names = [name for name in module_names if name not in onload_names]
|
|
onload_names = [name for name in module_names if name in onload_names]
|
|
|
|
for component_name in offload_names:
|
|
component = getattr(pipe, component_name)
|
|
if hasattr(component, "enable_group_offload"):
|
|
# For diffusers ModelMixin implementations
|
|
component.enable_group_offload(torch.device(torch_device), **group_offloading_kwargs)
|
|
else:
|
|
# For other models not part of diffusers
|
|
apply_group_offloading(component, onload_device=torch.device(torch_device), **group_offloading_kwargs)
|
|
assert all(
|
|
module._diffusers_hook.get_hook("group_offloading") is not None
|
|
for module in component.modules()
|
|
if hasattr(module, "_diffusers_hook")
|
|
)
|
|
for component_name in onload_names:
|
|
getattr(pipe, component_name).to(torch_device)
|
|
|
|
if group_offloading_kwargs.get("use_stream"):
|
|
self._assert_streams_took_effect(pipe, offload_names)
|
|
|
|
def _assert_streams_took_effect(self, pipe, offload_names):
|
|
"""Guard against `use_stream=True` silently becoming a no-op.
|
|
|
|
The count is pipeline-wide on purpose. A component can legitimately contribute zero: only groups built from
|
|
`ModuleList`/`Sequential` children get a stream, while the per-node unmatched group is always created with
|
|
`stream=None` because nothing precedes it to prefetch it. So a component with no such children streams
|
|
nothing — a `transformers` CLIP text encoder at block level, for instance — and that is not a defect.
|
|
Across the whole pipeline at least one group must stream, and asserting rather than skipping matters here:
|
|
if `use_stream` ever stopped being honoured, every count would fall to zero, and a skip would turn the
|
|
entire streaming suite green by omission instead of failing.
|
|
"""
|
|
# Several submodules share one `ModuleGroup`, so dedupe on group identity before counting.
|
|
has_stream = {}
|
|
for component_name in offload_names:
|
|
for submodule in getattr(pipe, component_name).modules():
|
|
registry = getattr(submodule, "_diffusers_hook", None)
|
|
hook = registry.get_hook("group_offloading") if registry is not None else None
|
|
if hook is not None:
|
|
has_stream[id(hook.group)] = hook.group.stream is not None
|
|
streamed = sum(has_stream.values())
|
|
assert streamed > 0, (
|
|
f"`use_stream=True` produced no streamed offload group across {sorted(offload_names)} for "
|
|
f"{self.pipeline_class.__name__}. Either the flag stopped taking effect, or none of these components "
|
|
f"has `ModuleList`/`Sequential` children to group into prefetchable blocks."
|
|
)
|
|
|
|
def _run_group_offload_inference(self, base_pipe_output, expected_max_difference, msg, **group_offloading_kwargs):
|
|
# Build the offload pipeline the same way as `base_pipe_output` so that group offloading is the only
|
|
# difference under test. It stays on CPU here — the components are placed as they are hooked.
|
|
pipe = self.create_pipe()
|
|
self._skip_if_group_offloading_unsupported(pipe)
|
|
self._enable_group_offload_on_components(pipe, **group_offloading_kwargs)
|
|
|
|
assert_tensors_close(self.run_pipe(pipe), base_pipe_output, atol=expected_max_difference, rtol=1e-5, msg=msg)
|
|
|
|
def _skip_if_streams_unsupported(self):
|
|
# `apply_group_offloading` raises rather than degrading when `use_stream=True` has nowhere to put a stream.
|
|
if not (torch.cuda.is_available() or (hasattr(torch, "xpu") and torch.xpu.is_available())):
|
|
pytest.skip("Group offloading with streams requires a CUDA or Intel XPU device.")
|
|
|
|
# NOTE: as marks do not carry through overrides, we can use `@GroupOffloadTesterMixin._USE_STREAM` or
|
|
# `@MemoryTestMixin._USE_STREAM` to parametrize overridden tests for streaming.
|
|
_USE_STREAM = pytest.mark.parametrize("use_stream", [False, True], ids=["no_stream", "stream"])
|
|
|
|
@require_torch_accelerator
|
|
@_USE_STREAM
|
|
def test_group_offloading_inference_block_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
|
|
if use_stream:
|
|
self._skip_if_streams_unsupported()
|
|
self._run_group_offload_inference(
|
|
base_pipe_output,
|
|
expected_max_difference,
|
|
msg="block-level group offloading should not affect the inference results",
|
|
offload_type="block_level",
|
|
num_blocks_per_group=1,
|
|
use_stream=use_stream,
|
|
)
|
|
|
|
@require_torch_accelerator
|
|
@_USE_STREAM
|
|
def test_group_offloading_inference_leaf_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
|
|
if use_stream:
|
|
self._skip_if_streams_unsupported()
|
|
self._run_group_offload_inference(
|
|
base_pipe_output,
|
|
expected_max_difference,
|
|
msg="leaf-level group offloading should not affect the inference results",
|
|
offload_type="leaf_level",
|
|
use_stream=use_stream,
|
|
)
|
|
|
|
@require_torch_accelerator
|
|
def test_pipeline_level_group_offloading_sanity_checks(self):
|
|
pipe: DiffusionPipeline = self.get_pipeline()
|
|
self._skip_if_group_offloading_unsupported(pipe)
|
|
|
|
module_names = sorted(
|
|
[name for name, component in pipe.components.items() if isinstance(component, torch.nn.Module)]
|
|
)
|
|
exclude_module_name = module_names[0]
|
|
offload_device = "cpu"
|
|
pipe.enable_group_offload(
|
|
onload_device=torch_device,
|
|
offload_device=offload_device,
|
|
offload_type="leaf_level",
|
|
exclude_modules=exclude_module_name,
|
|
)
|
|
excluded_module = getattr(pipe, exclude_module_name)
|
|
assert torch.device(excluded_module.device).type == torch.device(torch_device).type
|
|
|
|
for name, component in pipe.components.items():
|
|
if name not in [exclude_module_name] and isinstance(component, torch.nn.Module):
|
|
# `component.device` prints the `onload_device` type. We should probably override the `device`
|
|
# property in `ModelMixin`. Skip modules with no parameters (e.g., dummy safety checkers).
|
|
params = list(component.parameters())
|
|
if not params:
|
|
continue
|
|
component_device = params[0].device
|
|
assert torch.device(component_device).type == torch.device(offload_device).type
|
|
|
|
@require_torch_accelerator
|
|
def test_pipeline_level_group_offloading_inference(self, base_pipe_output, expected_max_difference=1e-4):
|
|
# Build the offload pipeline the same way as `base_pipe_output` so that group offloading is the only
|
|
# difference under test. It stays on CPU here — `enable_group_offload` places the components.
|
|
pipe: DiffusionPipeline = self.get_pipeline()
|
|
self._skip_if_group_offloading_unsupported(pipe)
|
|
|
|
offload_device = "cpu"
|
|
offload_type = "leaf_level"
|
|
pipe.enable_group_offload(
|
|
onload_device=torch_device,
|
|
offload_device=offload_device,
|
|
offload_type=offload_type,
|
|
exclude_modules=sorted(self._group_offload_exclude_modules(pipe, offload_type)),
|
|
)
|
|
pipe.set_progress_bar_config(disable=None)
|
|
inputs = self.get_dummy_inputs()
|
|
torch.manual_seed(0)
|
|
out_offload = pipe(**inputs)[0]
|
|
|
|
assert_tensors_close(
|
|
out_offload,
|
|
base_pipe_output,
|
|
atol=expected_max_difference,
|
|
msg="pipeline-level group offloading should not affect the inference results",
|
|
)
|
|
|
|
|
|
@is_memory
|
|
@require_accelerator
|
|
class MemoryTesterMixin(PipelineOffloadTesterMixin, GroupOffloadTesterMixin, LayerwiseCastingTesterMixin):
|
|
"""Umbrella mixin bundling all memory-placement tests (cf. model-level `MemoryTesterMixin`)."""
|