Skip to content

Commit 7252c84

Browse files
committed
resolve conflicts
2 parents ba560b5 + 9905327 commit 7252c84

18 files changed

Lines changed: 328 additions & 137 deletions

.ai/references/testing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ Follow the style introduced in [#14113](https://github.com/huggingface/diffusers
3030
- `MemoryTesterMixin` — CPU offload, group offload, layerwise casting.
3131
- Cache mixins — `PyramidAttentionBroadcastTesterMixin`, `FasterCacheTesterMixin`, `FirstBlockCacheTesterMixin`, `TaylorSeerCacheTesterMixin`, `MagCacheTesterMixin`. Guidance-distilled models override the cache config (e.g. `FASTER_CACHE_CONFIG = {... "is_guidance_distilled": True}`). Don't introduce caching related tests in the first iteration. These tests are added on a case-by-case basis.
3232
- In the first pass, just add tests related to `PipelineTesterMixin` and `MemoryTesterMixin`.
33-
- **Declare a component that can't be offloaded — don't hand-write a skip.** Leaf-level offloading hooks only the supported leaf types (`nn.Linear`, `nn.Conv*`, `nn.Embedding` — see `_GO_LC_SUPPORTED_PYTORCH_LAYERS` in `src/diffusers/hooks/_common.py`) and onloads each on its own `forward`, so any code that reads a leaf's `.weight` instead of calling the leaf bypasses that leaf's hook and computes against offloaded weights. Which fix applies depends on who owns the component. For a diffusers model, set `_supports_group_offloading = False` on the `ModelMixin` subclass (as `HunyuanDiT2DModel` does) — both offload mixins honor the flag and skip themselves, so the gap is declared on the model instead of buried in a test file. For a third-party component you can't annotate, such as a `transformers` encoder, list it in `group_offloading_leaf_level_exclude_modules` on the config class; `enable_group_offload` keeps excluded components on the accelerator, so every other component stays covered — including the VAE, which the component-scoped `test_group_offloading_inference` leaves out. Block-level offloading is usually unaffected, hence the level in the name — a component that fails at both levels does need a skip.
34-
- `torch.nn.MultiheadAttention` is the common instance: it passes `self.out_proj.weight` straight to `torch.nn.functional.multi_head_attention_forward` instead of calling `self.out_proj`, so the hook on `out_proj` never fires. `SiglipVisionModel`'s attention pooling head wraps one — see `tests/pipelines/hunyuan_video/test_hunyuan_video_framepack.py`, whose `image_encoder` is excluded for this reason.
33+
- **Declare a component that can't be offloaded — don't hand-write a skip.** For a diffusers model, set `_supports_group_offloading = False` on the `ModelMixin` subclass (as `HunyuanDiT2DModel` does); for a third-party component you can't annotate, such as a `transformers` encoder, name it in `group_offloading_leaf_level_exclude_modules` or `group_offloading_block_level_exclude_modules` on the config class. **Every `nn.Module` component is group offloaded unless the list for that level names it**, and the two levels fail on opposite hazards: leaf-level when compute reads a leaf's `.weight` instead of calling the leaf, block-level when a component re-enters submodules without going through the group leader's `forward` (VAE decode paths, hence the `vae` / `image_encoder` defaults). A component that fails at both goes in both, and a name matching no component fails the test as a typo. **Each level's test runs twice, with and without `use_stream`** — a subclass overriding one must re-declare `@MemoryTesterMixin._USE_STREAM`, or the override collapses to a single un-parametrized test that errors on the missing argument and reports as a green `xfail`.
34+
- `torch.nn.MultiheadAttention` is the common leaf-level instance: it passes `self.out_proj.weight` straight to `torch.nn.functional.multi_head_attention_forward` instead of calling `self.out_proj`, so the hook on `out_proj` never fires. `SiglipVisionModel`'s attention pooling head wraps one — see `tests/pipelines/hunyuan_video/test_hunyuan_video_framepack.py`, whose `image_encoder` is excluded for this reason.
3535
- `HunyuanDiTAttentionPool` (`src/diffusers/models/embeddings.py`) shows the same failure without an MHA module: a plain `nn.Module` that hands its `q_proj` / `k_proj` / `v_proj` / `c_proj` weights to `torch.nn.functional.multi_head_attention_forward`, so all four projections stay offloaded rather than just one. `HunyuanDiT2DModel` opts out of group offloading entirely with `_supports_group_offloading = False`.
3636
- Before adding a skip or an exclusion, confirm the failure still reproduces — several existing skips are stale, having outlived the upstream cause.
3737
- **A migration that surfaces a `src/` gap marks the test `xfail`, it does not patch the pipeline.** Give the marker a module-level name and a `reason` naming the exact gap (`PNDM_*` in `tests/pipelines/pndm/test_pndm.py` is the worked example), and prefer `strict=True` so the marker reports XPASS — and gets deleted — the day the pipeline is fixed. Use `strict=False` only when one mark covers a group whose members do not all fail. Marking a whole test class keeps the mixin's own marks (`@is_memory`, `@require_accelerator`) intact; overriding individual inherited tests drops the decorators they were declared with, so re-declare those too.

src/diffusers/models/transformers/transformer_prx.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,10 @@ def __call__(
161161
ones_img = torch.ones((bs, l_img), dtype=torch.bool, device=device)
162162
attention_mask = attention_mask.to(device=device, dtype=torch.bool)
163163
joint_mask = torch.cat([attention_mask, ones_img], dim=-1)
164-
attn_mask_tensor = joint_mask[:, None, None, :].expand(-1, attn.heads, l_img, -1)
164+
# Every attention backend broadcasts the mask itself, so materialising
165+
# [B, heads, L_img, L_all] only costs bandwidth: at batch 32, 1024 image tokens
166+
# and 28 heads that is a 1.1 GiB mask read per block. Keep it broadcastable.
167+
attn_mask_tensor = joint_mask[:, None, None, :]
165168

166169
# Apply attention using dispatch_attention_fn for backend support
167170
# Reshape to match dispatch_attention_fn expectations: [B, L, H, D]

src/diffusers/pipelines/cosmos/pipeline_cosmos3_omni.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ...callbacks import MultiPipelineCallbacks, PipelineCallback
2929
from ...models.autoencoders.autoencoder_cosmos3_audio import Cosmos3AVAEAudioTokenizer
3030
from ...models.autoencoders.autoencoder_kl_wan import AutoencoderKLWan
31+
from ...models.modeling_utils import get_parameter_device
3132
from ...models.transformers.transformer_cosmos3 import (
3233
Cosmos3OmniTransformer,
3334
)
@@ -489,7 +490,7 @@ def _get_execution_device(self) -> torch.device:
489490
return torch.device(execution_device)
490491

491492
try:
492-
return next(component.parameters()).device
493+
return get_parameter_device(component)
493494
except StopIteration:
494495
continue
495496

tests/pipelines/audioldm2/test_audioldm2.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,11 +469,17 @@ class TestAudioLDM2PipelineMemory(AudioLDM2PipelineTesterConfig, MemoryTesterMix
469469
def test_sequential_cpu_offload_forward_pass(self):
470470
pass
471471

472-
@pytest.mark.skip(
473-
"The pipeline encodes prompts through `text_encoder.get_text_features()` rather than the CLAP model's "
474-
"`forward()`, so the top-level group-offloading hook never fires and the embedding weights stay offloaded."
472+
# The pipeline encodes prompts through `text_encoder.get_text_features()` rather than the CLAP model's
473+
# `forward()`. Block-level offloading gates the ungrouped weights on that `forward`, so they stay on the offload
474+
# device; leaf level onloads each leaf on its own `forward` and is unaffected, so only block level is skipped.
475+
CLAP_BLOCK_OFFLOAD_SKIP = pytest.mark.skip(
476+
"`text_encoder.get_text_features()` bypasses the CLAP model's `forward()`, so block-level group offloading "
477+
"never onloads the embedding weights."
475478
)
476-
def test_group_offloading_inference(self):
479+
480+
@CLAP_BLOCK_OFFLOAD_SKIP
481+
@MemoryTesterMixin._USE_STREAM
482+
def test_group_offloading_inference_block_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
477483
pass
478484

479485
@pytest.mark.skip("Not supported yet due to CLAPModel.")

tests/pipelines/ideogram4/test_pipeline_ideogram4.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ class Ideogram4PipelineTesterConfig(BasePipelineTesterConfig):
5050
required_input_params_in_call_signature = frozenset(["prompt", "height", "width", "guidance_scale"])
5151
batch_input_params = frozenset(["prompt"])
5252
output_shape = (3, 16, 16)
53-
# `encode_prompt` drives the Qwen3-VL decoder layers directly instead of calling `text_encoder.forward`, so the
54-
# offloading hooks would leave its inputs on the offload device. Keep the text encoder out of group offloading.
53+
# `encode_prompt` calls the Qwen3-VL decoder layers directly instead of calling `text_encoder.forward`, and
54+
# pins its inputs to `self.text_encoder.device`. Leaf-level hooks onload each leaf on its own forward while the
55+
# module keeps reporting the offload device, so the inputs are left behind; block-level onloads the whole group
56+
# up front and is unaffected, which is where the text encoder does get covered.
5557
group_offloading_leaf_level_exclude_modules = ["text_encoder"]
5658

5759
def get_dummy_components(self, num_layers: int = 1):
@@ -285,7 +287,8 @@ class TestIdeogram4PipelineMemory(Ideogram4PipelineTesterConfig, MemoryTesterMix
285287
pins its inputs to `self.text_encoder.device` so they follow the weights under `enable_model_cpu_offload`
286288
(whose `CpuOffload` hook wraps the bypassed `forward` and so never fires). That pinning is wrong for every
287289
mechanism that hooks the submodules instead: they onload to the accelerator while the module still reports the
288-
offload device, so the inputs are left behind. Hence the skips below.
290+
offload device, so the inputs are left behind. Hence the skips below, and the text encoder's leaf-level group
291+
offload exclusion on the config class.
289292
"""
290293

291294
_SUBMODULE_OFFLOAD_SKIP = (
@@ -307,17 +310,6 @@ def test_sequential_cpu_offload_forward_pass(self, base_pipe_output, expected_ma
307310
def test_sequential_offload_forward_pass_twice(self, expected_max_diff=2e-4):
308311
pass
309312

310-
@pytest.mark.skip(
311-
reason=(
312-
"Block-level group offloading cannot cover `text_encoder`: it leaves ungrouped leaves such as "
313-
"`embed_tokens` to the root module's forward pre-hook, which never fires because `encode_prompt` "
314-
"drives the decoder layers directly. Leaf-level offloading hooks those leaves individually and is "
315-
"bit-exact here; only the block-level half of this test fails."
316-
)
317-
)
318-
def test_group_offloading_inference(self):
319-
pass
320-
321313

322314
class TestIdeogram4PipelineLoRA(Ideogram4PipelineTesterConfig, LoraTesterMixin):
323315
"""LoRA tests for the Ideogram4 pipeline."""

tests/pipelines/kandinsky/test_kandinsky_prior.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@
4646
"`clip_mean`, `clip_std`), which group offloading never onloads."
4747
)
4848

49+
# A second, independent gap: the component-scoped test only offloads the denoiser under the names
50+
# `transformer`/`unet`/`controlnet`/`adapter`, and only puts `vae`/`vqvae`/`image_encoder` back on the accelerator.
51+
# A prior pipeline's denoiser is called `prior`, so it matches neither list and is left on CPU while the text
52+
# encoder is onloaded. Fixing this means widening the mixin's component lists, not changing the pipeline.
53+
COMPONENT_GROUP_OFFLOAD_XFAIL_REASON = (
54+
"The pipeline calls `PriorTransformer.post_process_latents()` after the denoising loop, which reads the "
55+
"`clip_mean` / `clip_std` parameters held directly on the model. Group offloading onloads those only for the "
56+
"duration of `forward`, so by then they are back on the offload device."
57+
)
58+
4959

5060
class KandinskyPriorPipelineTesterConfig(BasePipelineTesterConfig):
5161
pipeline_class = KandinskyPriorPipeline
@@ -205,6 +215,20 @@ def test_inference_batch_single_identical(self, batch_size=3, expected_max_diff=
205215
class TestKandinskyPriorPipelineMemory(KandinskyPriorPipelineTesterConfig, MemoryTesterMixin):
206216
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the Kandinsky prior pipeline."""
207217

218+
@pytest.mark.xfail(condition=True, reason=COMPONENT_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
219+
@MemoryTesterMixin._USE_STREAM
220+
def test_group_offloading_inference_block_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
221+
super().test_group_offloading_inference_block_level(
222+
base_pipe_output, use_stream, expected_max_difference=expected_max_difference
223+
)
224+
225+
@pytest.mark.xfail(condition=True, reason=COMPONENT_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
226+
@MemoryTesterMixin._USE_STREAM
227+
def test_group_offloading_inference_leaf_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
228+
super().test_group_offloading_inference_leaf_level(
229+
base_pipe_output, use_stream, expected_max_difference=expected_max_difference
230+
)
231+
208232
@pytest.mark.xfail(condition=True, reason=PIPELINE_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
209233
def test_pipeline_level_group_offloading_inference(self, base_pipe_output, expected_max_difference=1e-4):
210234
super().test_pipeline_level_group_offloading_inference(

tests/pipelines/kandinsky2_2/test_kandinsky_prior.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@
4848
"`clip_mean`, `clip_std`), which group offloading never onloads."
4949
)
5050

51+
# A second, independent gap: the component-scoped test only offloads the denoiser under the names
52+
# `transformer`/`unet`/`controlnet`/`adapter`, and only puts `vae`/`vqvae`/`image_encoder` back on the accelerator.
53+
# A prior pipeline's denoiser is called `prior`, so it matches neither list and is left on CPU while the text
54+
# encoder is onloaded. Fixing this means widening the mixin's component lists, not changing the pipeline.
55+
COMPONENT_GROUP_OFFLOAD_XFAIL_REASON = (
56+
"The pipeline calls `PriorTransformer.post_process_latents()` after the denoising loop, which reads the "
57+
"`clip_mean` / `clip_std` parameters held directly on the model. Group offloading onloads those only for the "
58+
"duration of `forward`, so by then they are back on the offload device."
59+
)
60+
5161

5262
class KandinskyV22PriorPipelineTesterConfig(BasePipelineTesterConfig):
5363
pipeline_class = KandinskyV22PriorPipeline
@@ -240,6 +250,20 @@ class TestKandinskyV22PriorPipelineMemory(KandinskyV22PriorPipelineTesterConfig,
240250
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the Kandinsky 2.2 prior
241251
pipeline."""
242252

253+
@pytest.mark.xfail(condition=True, reason=COMPONENT_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
254+
@MemoryTesterMixin._USE_STREAM
255+
def test_group_offloading_inference_block_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
256+
super().test_group_offloading_inference_block_level(
257+
base_pipe_output, use_stream, expected_max_difference=expected_max_difference
258+
)
259+
260+
@pytest.mark.xfail(condition=True, reason=COMPONENT_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
261+
@MemoryTesterMixin._USE_STREAM
262+
def test_group_offloading_inference_leaf_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
263+
super().test_group_offloading_inference_leaf_level(
264+
base_pipe_output, use_stream, expected_max_difference=expected_max_difference
265+
)
266+
243267
@pytest.mark.xfail(condition=True, reason=PIPELINE_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
244268
def test_pipeline_level_group_offloading_inference(self, base_pipe_output, expected_max_difference=1e-4):
245269
super().test_pipeline_level_group_offloading_inference(

tests/pipelines/kandinsky2_2/test_kandinsky_prior_emb2emb.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@
5454
"`clip_mean`, `clip_std`), which group offloading never onloads."
5555
)
5656

57+
# A second, independent gap: the component-scoped test only offloads the denoiser under the names
58+
# `transformer`/`unet`/`controlnet`/`adapter`, and only puts `vae`/`vqvae`/`image_encoder` back on the accelerator.
59+
# A prior pipeline's denoiser is called `prior`, so it matches neither list and is left on CPU while the text
60+
# encoder is onloaded. Fixing this means widening the mixin's component lists, not changing the pipeline.
61+
COMPONENT_GROUP_OFFLOAD_XFAIL_REASON = (
62+
"The pipeline calls `PriorTransformer.post_process_latents()` after the denoising loop, which reads the "
63+
"`clip_mean` / `clip_std` parameters held directly on the model. Group offloading onloads those only for the "
64+
"duration of `forward`, so by then they are back on the offload device."
65+
)
66+
5767

5868
class KandinskyV22PriorEmb2EmbPipelineTesterConfig(BasePipelineTesterConfig):
5969
pipeline_class = KandinskyV22PriorEmb2EmbPipeline
@@ -224,6 +234,20 @@ class TestKandinskyV22PriorEmb2EmbPipelineMemory(KandinskyV22PriorEmb2EmbPipelin
224234
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the Kandinsky 2.2 prior
225235
emb2emb pipeline."""
226236

237+
@pytest.mark.xfail(condition=True, reason=COMPONENT_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
238+
@MemoryTesterMixin._USE_STREAM
239+
def test_group_offloading_inference_block_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
240+
super().test_group_offloading_inference_block_level(
241+
base_pipe_output, use_stream, expected_max_difference=expected_max_difference
242+
)
243+
244+
@pytest.mark.xfail(condition=True, reason=COMPONENT_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
245+
@MemoryTesterMixin._USE_STREAM
246+
def test_group_offloading_inference_leaf_level(self, base_pipe_output, use_stream, expected_max_difference=1e-4):
247+
super().test_group_offloading_inference_leaf_level(
248+
base_pipe_output, use_stream, expected_max_difference=expected_max_difference
249+
)
250+
227251
@pytest.mark.xfail(condition=True, reason=PIPELINE_GROUP_OFFLOAD_XFAIL_REASON, strict=True)
228252
def test_pipeline_level_group_offloading_inference(self, base_pipe_output, expected_max_difference=1e-4):
229253
super().test_pipeline_level_group_offloading_inference(

tests/pipelines/ltx2/test_pipeline_ltx2_dfr.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class LTX2DFRPipelineTesterConfig(BasePipelineTesterConfig):
4747
"return_dict",
4848
]
4949
)
50+
# This config subclasses `BasePipelineTesterConfig` rather than `LTX2BaseTesterConfig`, so it has to restate the
51+
# family's `audio_vae` exclusion: its decode-time convolutions run without the group leader's `forward` having
52+
# onloaded the group.
53+
group_offloading_block_level_exclude_modules = ["vae", "audio_vae"]
5054

5155
def get_dummy_components(self):
5256
return get_dfr_dummy_components()

tests/pipelines/ltx2/testing_utils.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
does not restate them.
2222
"""
2323

24-
import pytest
2524
import torch
2625
from transformers import AutoTokenizer, Gemma3ForConditionalGeneration
2726

@@ -283,6 +282,10 @@ class LTX2BaseTesterConfig(BasePipelineTesterConfig):
283282
# See `DEFAULT_UNSET_COMPONENTS`; each config narrows or widens this to the components its pipeline accepts.
284283
unset_components = DEFAULT_UNSET_COMPONENTS
285284

285+
# `audio_vae` fails at block level for the same reason the other VAEs do: its decode-time convolutions run
286+
# without the group leader's `forward` having onloaded the group.
287+
group_offloading_block_level_exclude_modules = ["vae", "audio_vae"]
288+
286289
def get_dummy_components(self):
287290
return get_ltx2_dummy_components(unset_components=self.unset_components)
288291

@@ -299,15 +302,6 @@ def get_pipeline_with_duration_head(self):
299302
class LTX2MemoryTesterMixin(MemoryTesterMixin):
300303
"""`MemoryTesterMixin` for the LTX2 pipelines in this directory."""
301304

302-
# The shared helper only group-offloads `text_encoder` / `transformer` and moves `vae`, leaving LTX2's extra
303-
# module components (`connectors`, `audio_vae`, `vocoder`) on the CPU while they receive accelerator tensors
304-
# from the offloaded text encoder, so the forward pass mixes devices. This is pre-existing for the whole LTX2
305-
# family rather than specific to any one pipeline. Pipeline-level offloading, which walks every component, is
306-
# exercised by `test_pipeline_level_group_offloading_inference`.
307-
@pytest.mark.skip("Using test_pipeline_level_group_offloading_inference instead")
308-
def test_group_offloading_inference(self):
309-
pass
310-
311305

312306
class LTX2LoraTesterMixin(LoraTesterMixin):
313307
"""`LoraTesterMixin` for the LTX2 pipelines in this directory.

0 commit comments

Comments
 (0)