1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-23 09:09:01 +03:00

[tests] refactor o and p series pipeline tests (#14646)

* refactor o and p series pipeline tests

* up
This commit is contained in:
Sayak Paul
2026-09-01 08:34:33 +05:30
committed by GitHub
parent a262497cd0
commit 61a11f2191
36 changed files with 1806 additions and 3105 deletions

View File

@@ -34,6 +34,10 @@ Follow the style introduced in [#14113](https://github.com/huggingface/diffusers
- `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.
- `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`.
- Before adding a skip or an exclusion, confirm the failure still reproduces — several existing skips are stale, having outlived the upstream cause.
- **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.
- **`from_pipe` tests** (a pipeline that is a variant of an existing one — PAG, AnimateDiff, ...) compose the shared `FromPipeTesterMixin` (`tests/pipelines/testing_utils/from_pipe.py`, exported from `..testing_utils`) in their own test class. It derives the original pipeline from `pipeline_class.__name__`; set `original_pipeline_repo` on the test class to pull it from a repo other than the default for that class. The unittest-era `PipelineFromPipeTesterMixin` in `tests/pipelines/test_pipelines_common.py` is what it replaces.
- **A hardware gap is a conditional skip, not an xfail.** When a test fails only because the runner's cuDNN build has no kernel for an op — `RuntimeError: GET was unable to find an engine to execute this computation`, as Sana's depthwise `Conv2d` hits in bfloat16 — wrap the call in `skip_if_no_cudnn_engine()` (`tests/testing_utils.py`). It skips on that error and re-raises every other `RuntimeError`, so the test still runs wherever the kernel exists.
- **PAG pipelines** also compose `PAGPipelineTesterMixin` (`tests/pipelines/pag/testing_utils.py`) in place of `PipelineTesterMixin`: it adds `test_pag_disable_enable` and `test_pag_inference` on top, driven by `base_pipeline_class` and the `pag_*` knobs on the test class. Keep `test_pag_applied_layers` per pipeline — which layers PAG resolves to is model-specific.
- **`encode_prompt` reading a component that isn't a text encoder or tokenizer?** `test_encode_prompt_works_in_isolation` rebuilds the pipeline with only the components whose names contain `text` or `tokenizer`. When `encode_prompt` also needs another one — a `processor` used for chat templating, say — list it in `text_stack_component_names` on the config class rather than re-implementing the test.
- **IP-Adapter tests** live in their own class decorated with `@is_ip_adapter`, subclassing only the config (not `PipelineTesterMixin`). UNet pipelines that load adapters through the standard `IPAdapterMixin` API compose the shared `IPAdapterTesterMixin` (`tests/pipelines/testing_utils/ip_adapter.py`, exported from `..testing_utils`); pipelines whose IP-Adapter API differs (Flux, for example) keep a bespoke mixin next to their own tests.

View File

@@ -116,9 +116,7 @@ def retrieve_timesteps(
return timesteps, num_inference_steps
class OmniGenPipeline(
DiffusionPipeline,
):
class OmniGenPipeline(DiffusionPipeline):
r"""
The OmniGen pipeline for multimodal-to-image generation.

View File

@@ -10,7 +10,6 @@ from diffusers import (
AutoencoderKL,
DDIMScheduler,
MotionAdapter,
StableDiffusionPipeline,
UNet2DConditionModel,
)
@@ -22,8 +21,8 @@ from ...testing_utils import (
torch_device,
)
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineFromPipeTesterMixin
from ..testing_utils import (
FromPipeTesterMixin,
IPAdapterTesterMixin,
LoraMemoryTesterMixin,
LoraTesterMixin,
@@ -31,7 +30,6 @@ from ..testing_utils import (
UNetLoraTesterMixin,
)
from .testing_utils import (
FROM_PIPE_SKIP_REASON,
FreeInitTesterMixin,
FreeNoiseSplitInferenceTesterMixin,
MotionPipelineTesterConfig,
@@ -128,32 +126,6 @@ class TestAnimateDiffPipeline(
FreeInitTesterMixin,
FreeNoiseSplitInferenceTesterMixin,
):
def test_from_pipe_consistent_config(self):
original_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"
# create StableDiffusionPipeline
pipe_original = StableDiffusionPipeline.from_pretrained(original_repo, requires_safety_checker=False)
# StableDiffusionPipeline -> AnimateDiffPipeline
pipe_components = self.get_dummy_components()
pipe_additional_components = {
name: component for name, component in pipe_components.items() if name not in pipe_original.components
}
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# AnimateDiffPipeline -> StableDiffusionPipeline
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = StableDiffusionPipeline.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_dict_tuple_outputs_equivalent(self, expected_slice=None, expected_max_difference=1e-4):
if torch_device == "cpu" and expected_slice is None:
# fmt: off
@@ -244,10 +216,7 @@ class TestAnimateDiffPipelineIntegration:
assert numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice.flatten()) < 1e-3
@pytest.mark.skip(FROM_PIPE_SKIP_REASON)
class TestAnimateDiffPipelineFromPipe(AnimateDiffPipelineTesterConfig, PipelineFromPipeTesterMixin):
"""`from_pipe` forward-pass parity and offload round trip for the AnimateDiff pipeline.
class TestAnimateDiffPipelineFromPipe(AnimateDiffPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline` for the AnimateDiff pipeline."""
Parked, not deleted: `test_from_pipe_consistent_config` runs for real as a method on the main test class above,
but the forward-pass checks in `PipelineFromPipeTesterMixin` have no pytest-style equivalent yet.
"""
original_pipeline_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"

View File

@@ -1,4 +1,3 @@
import pytest
import torch
from PIL import Image
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -9,14 +8,13 @@ from diffusers import (
ControlNetModel,
DDIMScheduler,
MotionAdapter,
StableDiffusionPipeline,
UNet2DConditionModel,
)
from ...testing_utils import torch_device
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineFromPipeTesterMixin
from ..testing_utils import (
FromPipeTesterMixin,
IPAdapterTesterMixin,
LoraMemoryTesterMixin,
LoraTesterMixin,
@@ -24,7 +22,6 @@ from ..testing_utils import (
UNetLoraTesterMixin,
)
from .testing_utils import (
FROM_PIPE_SKIP_REASON,
FreeInitTesterMixin,
FreeNoiseTesterMixin,
MotionPipelineTesterConfig,
@@ -142,32 +139,6 @@ class TestAnimateDiffControlNetPipeline(
# `get_dummy_inputs` rather than by overriding `num_frames` on the returned dict.
return self.get_dummy_inputs(num_frames=16)
def test_from_pipe_consistent_config(self):
original_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"
# create StableDiffusionPipeline
pipe_original = StableDiffusionPipeline.from_pretrained(original_repo, requires_safety_checker=False)
# StableDiffusionPipeline -> AnimateDiffControlNetPipeline
pipe_components = self.get_dummy_components()
pipe_additional_components = {
name: component for name, component in pipe_components.items() if name not in pipe_original.components
}
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# AnimateDiffControlNetPipeline -> StableDiffusionPipeline
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = StableDiffusionPipeline.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_dict_tuple_outputs_equivalent(self, expected_slice=None, expected_max_difference=1e-4):
if torch_device == "cpu" and expected_slice is None:
# fmt: off
@@ -198,12 +169,7 @@ class TestAnimateDiffControlNetPipelineLoRAMemory(AnimateDiffControlNetPipelineT
"""LoRA x memory-optimization tests (group offload, CPU offload) for the pipeline."""
@pytest.mark.skip(FROM_PIPE_SKIP_REASON)
class TestAnimateDiffControlNetPipelineFromPipe(
AnimateDiffControlNetPipelineTesterConfig, PipelineFromPipeTesterMixin
):
"""`from_pipe` forward-pass parity and offload round trip for the AnimateDiff ControlNet pipeline.
class TestAnimateDiffControlNetPipelineFromPipe(AnimateDiffControlNetPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline` for the AnimateDiff ControlNet pipeline."""
Parked, not deleted: `test_from_pipe_consistent_config` runs for real as a method on the main test class above,
but the forward-pass checks in `PipelineFromPipeTesterMixin` have no pytest-style equivalent yet.
"""
original_pipeline_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"

View File

@@ -1,4 +1,3 @@
import pytest
import torch
from PIL import Image
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -9,14 +8,13 @@ from diffusers import (
DDIMScheduler,
MotionAdapter,
SparseControlNetModel,
StableDiffusionPipeline,
UNet2DConditionModel,
)
from ...testing_utils import assert_tensors_close, torch_device
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineFromPipeTesterMixin
from ..testing_utils import (
FromPipeTesterMixin,
IPAdapterTesterMixin,
LoraMemoryTesterMixin,
LoraTesterMixin,
@@ -24,7 +22,6 @@ from ..testing_utils import (
UNetLoraTesterMixin,
)
from .testing_utils import (
FROM_PIPE_SKIP_REASON,
FreeInitTesterMixin,
MotionPipelineTesterConfig,
MotionPipelineTesterMixin,
@@ -138,32 +135,6 @@ class TestAnimateDiffSparseControlNetPipeline(
MotionPipelineTesterMixin,
FreeInitTesterMixin,
):
def test_from_pipe_consistent_config(self):
original_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"
# create StableDiffusionPipeline
pipe_original = StableDiffusionPipeline.from_pretrained(original_repo, requires_safety_checker=False)
# StableDiffusionPipeline -> AnimateDiffSparseControlNetPipeline
pipe_components = self.get_dummy_components()
pipe_additional_components = {
name: component for name, component in pipe_components.items() if name not in pipe_original.components
}
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# AnimateDiffSparseControlNetPipeline -> StableDiffusionPipeline
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = StableDiffusionPipeline.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_dict_tuple_outputs_equivalent(self, expected_slice=None, expected_max_difference=1e-4):
if torch_device == "cpu" and expected_slice is None:
# fmt: off
@@ -229,12 +200,9 @@ class TestAnimateDiffSparseControlNetPipelineLoRAMemory(
"""LoRA x memory-optimization tests (group offload, CPU offload) for the pipeline."""
@pytest.mark.skip(FROM_PIPE_SKIP_REASON)
class TestAnimateDiffSparseControlNetPipelineFromPipe(
AnimateDiffSparseControlNetPipelineTesterConfig, PipelineFromPipeTesterMixin
AnimateDiffSparseControlNetPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` forward-pass parity and offload round trip for the AnimateDiff SparseControlNet pipeline.
"""`from_pipe` round-trip tests against `StableDiffusionPipeline` for the AnimateDiff SparseControlNet pipeline."""
Parked, not deleted: `test_from_pipe_consistent_config` runs for real as a method on the main test class above,
but the forward-pass checks in `PipelineFromPipeTesterMixin` have no pytest-style equivalent yet.
"""
original_pipeline_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"

View File

@@ -1,4 +1,3 @@
import pytest
import torch
from PIL import Image
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -8,14 +7,13 @@ from diffusers import (
AutoencoderKL,
DDIMScheduler,
MotionAdapter,
StableDiffusionPipeline,
UNet2DConditionModel,
)
from ...testing_utils import torch_device
from ..pipeline_params import TEXT_TO_IMAGE_PARAMS, VIDEO_TO_VIDEO_BATCH_PARAMS
from ..test_pipelines_common import PipelineFromPipeTesterMixin
from ..testing_utils import (
FromPipeTesterMixin,
IPAdapterTesterMixin,
LoraMemoryTesterMixin,
LoraTesterMixin,
@@ -23,7 +21,6 @@ from ..testing_utils import (
UNetLoraTesterMixin,
)
from .testing_utils import (
FROM_PIPE_SKIP_REASON,
FreeInitTesterMixin,
FreeNoiseSplitInferenceTesterMixin,
MotionPipelineTesterConfig,
@@ -133,32 +130,6 @@ class TestAnimateDiffVideoToVideoPipeline(
inputs["strength"] = 0.5
return inputs
def test_from_pipe_consistent_config(self):
original_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"
# create StableDiffusionPipeline
pipe_original = StableDiffusionPipeline.from_pretrained(original_repo, requires_safety_checker=False)
# StableDiffusionPipeline -> AnimateDiffVideoToVideoPipeline
pipe_components = self.get_dummy_components()
pipe_additional_components = {
name: component for name, component in pipe_components.items() if name not in pipe_original.components
}
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# AnimateDiffVideoToVideoPipeline -> StableDiffusionPipeline
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = StableDiffusionPipeline.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_latent_inputs(self):
pipe = self.get_pipeline().to(torch_device)
@@ -191,12 +162,7 @@ class TestAnimateDiffVideoToVideoPipelineLoRAMemory(
"""LoRA x memory-optimization tests (group offload, CPU offload) for the pipeline."""
@pytest.mark.skip(FROM_PIPE_SKIP_REASON)
class TestAnimateDiffVideoToVideoPipelineFromPipe(
AnimateDiffVideoToVideoPipelineTesterConfig, PipelineFromPipeTesterMixin
):
"""`from_pipe` forward-pass parity and offload round trip for the AnimateDiff video-to-video pipeline.
class TestAnimateDiffVideoToVideoPipelineFromPipe(AnimateDiffVideoToVideoPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline` for the AnimateDiff video-to-video pipeline."""
Parked, not deleted: `test_from_pipe_consistent_config` runs for real as a method on the main test class above,
but the forward-pass checks in `PipelineFromPipeTesterMixin` have no pytest-style equivalent yet.
"""
original_pipeline_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"

View File

@@ -1,4 +1,3 @@
import pytest
import torch
from PIL import Image
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -9,14 +8,13 @@ from diffusers import (
ControlNetModel,
DDIMScheduler,
MotionAdapter,
StableDiffusionPipeline,
UNet2DConditionModel,
)
from ...testing_utils import torch_device
from ..pipeline_params import TEXT_TO_IMAGE_PARAMS, VIDEO_TO_VIDEO_BATCH_PARAMS
from ..test_pipelines_common import PipelineFromPipeTesterMixin
from ..testing_utils import (
FromPipeTesterMixin,
IPAdapterTesterMixin,
LoraMemoryTesterMixin,
LoraTesterMixin,
@@ -24,7 +22,6 @@ from ..testing_utils import (
UNetLoraTesterMixin,
)
from .testing_utils import (
FROM_PIPE_SKIP_REASON,
FreeInitTesterMixin,
FreeNoiseTesterMixin,
MotionPipelineTesterConfig,
@@ -145,32 +142,6 @@ class TestAnimateDiffVideoToVideoControlNetPipeline(
# longer video rather than by passing `num_frames`.
return self.get_dummy_inputs(num_frames=16)
def test_from_pipe_consistent_config(self):
original_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"
# create StableDiffusionPipeline
pipe_original = StableDiffusionPipeline.from_pretrained(original_repo, requires_safety_checker=False)
# StableDiffusionPipeline -> AnimateDiffVideoToVideoControlNetPipeline
pipe_components = self.get_dummy_components()
pipe_additional_components = {
name: component for name, component in pipe_components.items() if name not in pipe_original.components
}
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# AnimateDiffVideoToVideoControlNetPipeline -> StableDiffusionPipeline
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = StableDiffusionPipeline.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_latent_inputs(self):
pipe = self.get_pipeline().to(torch_device)
@@ -213,12 +184,9 @@ class TestAnimateDiffVideoToVideoControlNetPipelineLoRAMemory(
"""LoRA x memory-optimization tests (group offload, CPU offload) for the pipeline."""
@pytest.mark.skip(FROM_PIPE_SKIP_REASON)
class TestAnimateDiffVideoToVideoControlNetPipelineFromPipe(
AnimateDiffVideoToVideoControlNetPipelineTesterConfig, PipelineFromPipeTesterMixin
AnimateDiffVideoToVideoControlNetPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` forward-pass parity and offload round trip for the AnimateDiff video-to-video ControlNet pipeline.
"""`from_pipe` round-trip tests against `StableDiffusionPipeline` for the AnimateDiff video-to-video ControlNet pipeline."""
Parked, not deleted: `test_from_pipe_consistent_config` runs for real as a method on the main test class above,
but the forward-pass checks in `PipelineFromPipeTesterMixin` have no pytest-style equivalent yet.
"""
original_pipeline_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"

View File

@@ -24,19 +24,6 @@ from ..testing_utils import BasePipelineTesterConfig, PipelineTesterMixin
from ..testing_utils.common import BasePipelineOutputMixin
# `PipelineFromPipeTesterMixin` (tests/pipelines/test_pipelines_common.py) still covers `from_pipe` forward-pass
# parity and the model-CPU-offload round trip, but it is unittest-era: its tests call `self.get_dummy_inputs(device,
# seed=0)` and `self.assertLess`, neither of which exists on a `BasePipelineTesterConfig` outside a
# `unittest.TestCase`. Un-skipping the parked classes below without porting the mixin first would error, not fail.
# The mixin is still live for the ten `tests/pipelines/pag/` files and `stable_diffusion_adapter`; rewrite it
# pytest-style when those are migrated, then drop these skips.
FROM_PIPE_SKIP_REASON = (
"`PipelineFromPipeTesterMixin` is still unittest-style and cannot run against `BasePipelineTesterConfig` — "
"these error rather than fail if un-skipped. Port the mixin to pytest (due when `tests/pipelines/pag/` is "
"migrated), then remove this skip."
)
class MotionPipelineTesterConfig(BasePipelineTesterConfig):
"""`BasePipelineTesterConfig` for the AnimateDiff pipelines in this directory."""

View File

@@ -1,7 +1,7 @@
import gc
import unittest
import numpy as np
import pytest
import torch
from transformers import AutoTokenizer
@@ -15,15 +15,14 @@ from ...testing_utils import (
slow,
torch_device,
)
from ..test_pipelines_common import PipelineTesterMixin
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin
class OmniGenPipelineFastTests(unittest.TestCase, PipelineTesterMixin):
class OmniGenPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = OmniGenPipeline
params = frozenset(["prompt", "guidance_scale"])
batch_params = frozenset(["prompt"])
test_xformers_attention = False
test_layerwise_casting = True
required_input_params_in_call_signature = frozenset(["prompt", "guidance_scale"])
batch_input_params = frozenset(["prompt"])
output_shape = (3, 16, 16)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -54,53 +53,47 @@ class OmniGenPipelineFastTests(unittest.TestCase, PipelineTesterMixin):
scheduler = FlowMatchEulerDiscreteScheduler(invert_sigmas=True, num_train_timesteps=1)
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/llama-tokenizer")
components = {
return {
"transformer": transformer,
"vae": vae,
"scheduler": scheduler,
"tokenizer": tokenizer,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device="cpu").manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 1,
"guidance_scale": 3.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"height": 16,
"width": 16,
}
return inputs
def test_inference(self):
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
generated_image = pipe(**inputs).images[0]
class TestOmniGenPipeline(OmniGenPipelineTesterConfig, PipelineTesterMixin):
"""Core pipeline tests for OmniGen. The old `test_inference` only asserted the output shape, which
`PipelineTesterMixin.test_output` now covers against `output_shape`."""
self.assertEqual(generated_image.shape, (16, 16, 3))
class TestOmniGenPipelineMemory(OmniGenPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the OmniGen pipeline."""
@slow
@require_torch_accelerator
class OmniGenPipelineSlowTests(unittest.TestCase):
class TestOmniGenPipelineIntegration:
pipeline_class = OmniGenPipeline
repo_id = "shitao/OmniGen-v1-diffusers"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -13,9 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import numpy as np
import torch
from transformers import Qwen2Tokenizer, Qwen3Config, Qwen3Model
@@ -27,29 +24,15 @@ from diffusers import (
)
from ...testing_utils import torch_device
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_IMAGE_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineTesterMixin
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin
class OvisImagePipelineFastTests(PipelineTesterMixin, unittest.TestCase):
class OvisImagePipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = OvisImagePipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params = frozenset(
[
"num_inference_steps",
"generator",
"latents",
"return_dict",
"callback_on_step_end",
"callback_on_step_end_tensor_inputs",
]
)
test_xformers_attention = False
test_layerwise_casting = True
test_group_offloading = True
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
output_shape = (3, 16, 16)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -101,47 +84,37 @@ class OvisImagePipelineFastTests(PipelineTesterMixin, unittest.TestCase):
"transformer": transformer,
}
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
def get_dummy_inputs(self):
return {
"prompt": "a cat",
"negative_prompt": "bad quality",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 2.0,
"height": 16,
"width": 16,
"max_sequence_length": 32,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
def test_inference(self):
device = "cpu"
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
generated_image = image[0]
self.assertEqual(generated_image.shape, (16, 16, 3))
self.assertTrue(np.isfinite(image).all())
class TestOvisImagePipeline(OvisImagePipelineTesterConfig, PipelineTesterMixin):
def test_inference(self, base_pipe_output):
# `test_output` already pins the shape; this one guards against a NaN/inf output.
assert torch.isfinite(base_pipe_output).all()
def test_guidance_scale_is_set(self):
# The `guidance_scale` property reads `self._guidance_scale`, which `__call__` must initialize.
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
pipe = self.get_pipeline().to(torch_device)
inputs = self.get_dummy_inputs()
pipe(**inputs)
assert pipe.guidance_scale == inputs["guidance_scale"]
def test_max_sequence_length_is_used(self):
# `max_sequence_length` should bound the encoded prompt length.
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
pipe = self.get_pipeline().to(torch_device)
embeds_16 = pipe.encode_prompt(
"a cat", do_classifier_free_guidance=False, device=torch_device, max_sequence_length=16
)[0]
@@ -150,3 +123,7 @@ class OvisImagePipelineFastTests(PipelineTesterMixin, unittest.TestCase):
)[0]
assert embeds_16.shape[1] == 16
assert embeds_32.shape[1] == 32
class TestOvisImagePipelineMemory(OvisImagePipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the OvisImage pipeline."""

View File

@@ -1,7 +1,4 @@
import inspect
import unittest
import numpy as np
import pytest
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -10,49 +7,28 @@ from diffusers import (
AnimateDiffPipeline,
AutoencoderKL,
DDIMScheduler,
DPMSolverMultistepScheduler,
LCMScheduler,
MotionAdapter,
StableDiffusionPipeline,
UNet2DConditionModel,
UNetMotionModel,
)
from diffusers.models.attention import FreeNoiseTransformerBlock
from diffusers.utils import is_xformers_available
from ...testing_utils import require_accelerator, torch_device
from ...testing_utils import torch_device
from ..animatediff.testing_utils import (
FreeInitTesterMixin,
FreeNoiseTesterMixin,
MotionPipelineTesterConfig,
MotionPipelineTesterMixin,
)
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import (
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineTesterMixin,
SDFunctionTesterMixin,
)
from ..testing_utils import FromPipeTesterMixin, IPAdapterTesterMixin, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
def to_np(tensor):
if isinstance(tensor, torch.Tensor):
tensor = tensor.detach().cpu().numpy()
return tensor
class AnimateDiffPAGPipelineFastTests(
IPAdapterTesterMixin, SDFunctionTesterMixin, PipelineTesterMixin, PipelineFromPipeTesterMixin, unittest.TestCase
):
class AnimateDiffPAGPipelineTesterConfig(MotionPipelineTesterConfig):
pipeline_class = AnimateDiffPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
required_optional_params = frozenset(
[
"num_inference_steps",
"generator",
"latents",
"return_dict",
"callback_on_step_end",
"callback_on_step_end_tensor_inputs",
]
)
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
# `num_frames` defaults to 16; height/width default to `unet.sample_size * vae_scale_factor` (8 * 2).
output_shape = (16, 3, 16, 16)
def get_dummy_components(self):
cross_attention_dim = 8
@@ -100,6 +76,7 @@ class AnimateDiffPAGPipelineFastTests(
)
text_encoder = CLIPTextModel(text_encoder_config)
tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
torch.manual_seed(0)
motion_adapter = MotionAdapter(
block_out_channels=block_out_channels,
motion_layers_per_block=2,
@@ -107,7 +84,7 @@ class AnimateDiffPAGPipelineFastTests(
motion_num_attention_heads=4,
)
components = {
return {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
@@ -117,375 +94,51 @@ class AnimateDiffPAGPipelineFastTests(
"feature_extractor": None,
"image_encoder": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 7.5,
"pag_scale": 3.0,
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
"output_type": "pt",
}
return inputs
def test_from_pipe_consistent_config(self):
assert self.original_pipeline_class == StableDiffusionPipeline
original_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"
original_kwargs = {"requires_safety_checker": False}
# create original_pipeline_class(sd)
pipe_original = self.original_pipeline_class.from_pretrained(original_repo, **original_kwargs)
class TestAnimateDiffPAGPipeline(
AnimateDiffPAGPipelineTesterConfig,
MotionPipelineTesterMixin,
PAGPipelineTesterMixin,
FreeInitTesterMixin,
FreeNoiseTesterMixin,
):
base_pipeline_class = AnimateDiffPipeline
# AnimateDiff's PAG layers resolve through the motion modules, so the "PAG enabled" leg keeps the pipeline
# default layers and just leaves `pag_scale` at the dummy inputs' 3.0.
pag_enabled_applied_layers = None
# original_pipeline_class(sd) -> pipeline_class
pipe_components = self.get_dummy_components()
pipe_additional_components = {}
for name, component in pipe_components.items():
if name not in pipe_original.components:
pipe_additional_components[name] = component
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# pipeline_class -> original_pipeline_class(sd)
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = self.original_pipeline_class.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_motion_unet_loading(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
assert isinstance(pipe.unet, UNetMotionModel)
@unittest.skip("Attention slicing is not enabled in this pipeline")
def test_attention_slicing_forward_pass(self):
@pytest.mark.skip(
"`AnimateDiffPAGPipeline.check_inputs` rejects a dict `prompt`, so FreeNoise's per-frame prompts cannot be "
"passed to it (the non-PAG `AnimateDiffPipeline` accepts them)."
)
def test_free_noise_multi_prompt(self):
pass
def test_ip_adapter(self):
expected_pipe_slice = None
if torch_device == "cpu":
expected_pipe_slice = np.array(
[
0.5254,
0.5844,
0.4705,
0.4952,
0.4953,
0.5932,
0.5227,
0.4285,
0.5430,
0.4903,
0.4027,
0.4942,
0.3771,
0.3899,
0.6014,
0.4469,
0.4935,
0.5543,
0.5867,
0.5314,
0.3727,
0.5183,
0.6138,
0.5170,
0.5111,
0.4725,
0.5741,
]
)
return super().test_ip_adapter(expected_pipe_slice=expected_pipe_slice)
def test_dict_tuple_outputs_equivalent(self):
expected_slice = None
if torch_device == "cpu":
expected_slice = np.array([0.5227, 0.4285, 0.5430, 0.4469, 0.4935, 0.5543, 0.5111, 0.4725, 0.5741])
return super().test_dict_tuple_outputs_equivalent(expected_slice=expected_slice)
@require_accelerator
def test_to_device(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to("cpu")
# pipeline creates a new motion UNet under the hood. So we need to check the device from pipe.components
model_devices = [
component.device.type for component in pipe.components.values() if hasattr(component, "device")
]
self.assertTrue(all(device == "cpu" for device in model_devices))
output_cpu = pipe(**self.get_dummy_inputs("cpu"))[0]
self.assertTrue(np.isnan(output_cpu).sum() == 0)
pipe.to(torch_device)
model_devices = [
component.device.type for component in pipe.components.values() if hasattr(component, "device")
]
self.assertTrue(all(device == torch_device for device in model_devices))
output_device = pipe(**self.get_dummy_inputs(torch_device))[0]
self.assertTrue(np.isnan(to_np(output_device)).sum() == 0)
def test_to_dtype(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
# pipeline creates a new motion UNet under the hood. So we need to check the dtype from pipe.components
model_dtypes = [component.dtype for component in pipe.components.values() if hasattr(component, "dtype")]
self.assertTrue(all(dtype == torch.float32 for dtype in model_dtypes))
pipe.to(dtype=torch.float16)
model_dtypes = [component.dtype for component in pipe.components.values() if hasattr(component, "dtype")]
self.assertTrue(all(dtype == torch.float16 for dtype in model_dtypes))
def test_prompt_embeds(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
inputs.pop("prompt")
inputs["prompt_embeds"] = torch.randn((1, 4, pipe.text_encoder.config.hidden_size), device=torch_device)
pipe(**inputs)
def test_free_init(self):
components = self.get_dummy_components()
pipe: AnimateDiffPAGPipeline = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to(torch_device)
inputs_normal = self.get_dummy_inputs(torch_device)
frames_normal = pipe(**inputs_normal).frames[0]
pipe.enable_free_init(
num_iters=2,
use_fast_sampling=True,
method="butterworth",
order=4,
spatial_stop_frequency=0.25,
temporal_stop_frequency=0.25,
def test_dict_tuple_outputs_equivalent(self, expected_slice=None, expected_max_difference=1e-4):
# Re-recorded: `get_dummy_components` now seeds the `MotionAdapter` like every other component, which it
# did not before the migration, so the adapter's weights (and this slice) changed.
if torch_device == "cpu" and expected_slice is None:
# fmt: off
expected_slice = torch.tensor([0.5132, 0.4380, 0.5327, 0.4619, 0.4955, 0.5457, 0.4980, 0.5015, 0.5652])
# fmt: on
super().test_dict_tuple_outputs_equivalent(
expected_slice=expected_slice, expected_max_difference=expected_max_difference
)
inputs_enable_free_init = self.get_dummy_inputs(torch_device)
frames_enable_free_init = pipe(**inputs_enable_free_init).frames[0]
pipe.disable_free_init()
inputs_disable_free_init = self.get_dummy_inputs(torch_device)
frames_disable_free_init = pipe(**inputs_disable_free_init).frames[0]
sum_enabled = np.abs(to_np(frames_normal) - to_np(frames_enable_free_init)).sum()
max_diff_disabled = np.abs(to_np(frames_normal) - to_np(frames_disable_free_init)).max()
self.assertGreater(
sum_enabled, 1e1, "Enabling of FreeInit should lead to results different from the default pipeline results"
)
self.assertLess(
max_diff_disabled,
1e-3,
"Disabling of FreeInit should lead to results similar to the default pipeline results",
)
def test_free_init_with_schedulers(self):
components = self.get_dummy_components()
pipe: AnimateDiffPAGPipeline = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to(torch_device)
inputs_normal = self.get_dummy_inputs(torch_device)
frames_normal = pipe(**inputs_normal).frames[0]
schedulers_to_test = [
DPMSolverMultistepScheduler.from_config(
components["scheduler"].config,
timestep_spacing="linspace",
beta_schedule="linear",
algorithm_type="dpmsolver++",
steps_offset=1,
clip_sample=False,
),
LCMScheduler.from_config(
components["scheduler"].config,
timestep_spacing="linspace",
beta_schedule="linear",
steps_offset=1,
clip_sample=False,
),
]
components.pop("scheduler")
for scheduler in schedulers_to_test:
components["scheduler"] = scheduler
pipe: AnimateDiffPAGPipeline = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to(torch_device)
pipe.enable_free_init(num_iters=2, use_fast_sampling=False)
inputs = self.get_dummy_inputs(torch_device)
frames_enable_free_init = pipe(**inputs).frames[0]
sum_enabled = np.abs(to_np(frames_normal) - to_np(frames_enable_free_init)).sum()
self.assertGreater(
sum_enabled,
1e1,
"Enabling of FreeInit should lead to results different from the default pipeline results",
)
def test_free_noise_blocks(self):
components = self.get_dummy_components()
pipe: AnimateDiffPAGPipeline = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to(torch_device)
pipe.enable_free_noise()
for block in pipe.unet.down_blocks:
for motion_module in block.motion_modules:
for transformer_block in motion_module.transformer_blocks:
self.assertTrue(
isinstance(transformer_block, FreeNoiseTransformerBlock),
"Motion module transformer blocks must be an instance of `FreeNoiseTransformerBlock` after enabling FreeNoise.",
)
pipe.disable_free_noise()
for block in pipe.unet.down_blocks:
for motion_module in block.motion_modules:
for transformer_block in motion_module.transformer_blocks:
self.assertFalse(
isinstance(transformer_block, FreeNoiseTransformerBlock),
"Motion module transformer blocks must not be an instance of `FreeNoiseTransformerBlock` after disabling FreeNoise.",
)
def test_free_noise(self):
components = self.get_dummy_components()
pipe: AnimateDiffPAGPipeline = self.pipeline_class(**components)
pipe.set_progress_bar_config(disable=None)
pipe.to(torch_device)
inputs_normal = self.get_dummy_inputs(torch_device)
frames_normal = pipe(**inputs_normal).frames[0]
for context_length in [8, 9]:
for context_stride in [4, 6]:
pipe.enable_free_noise(context_length, context_stride)
inputs_enable_free_noise = self.get_dummy_inputs(torch_device)
frames_enable_free_noise = pipe(**inputs_enable_free_noise).frames[0]
pipe.disable_free_noise()
inputs_disable_free_noise = self.get_dummy_inputs(torch_device)
frames_disable_free_noise = pipe(**inputs_disable_free_noise).frames[0]
sum_enabled = np.abs(to_np(frames_normal) - to_np(frames_enable_free_noise)).sum()
max_diff_disabled = np.abs(to_np(frames_normal) - to_np(frames_disable_free_noise)).max()
self.assertGreater(
sum_enabled,
1e1,
"Enabling of FreeNoise should lead to results different from the default pipeline results",
)
self.assertLess(
max_diff_disabled,
1e-4,
"Disabling of FreeNoise should lead to results similar to the default pipeline results",
)
@unittest.skipIf(
torch_device != "cuda" or not is_xformers_available(),
reason="XFormers attention is only available with CUDA and `xformers` installed",
)
def test_xformers_attention_forwardGenerator_pass(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
for component in pipe.components.values():
if hasattr(component, "set_default_attn_processor"):
component.set_default_attn_processor()
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(torch_device)
output_without_offload = pipe(**inputs).frames[0]
output_without_offload = (
output_without_offload.cpu() if torch.is_tensor(output_without_offload) else output_without_offload
)
pipe.enable_xformers_memory_efficient_attention()
inputs = self.get_dummy_inputs(torch_device)
output_with_offload = pipe(**inputs).frames[0]
output_with_offload = (
output_with_offload.cpu() if torch.is_tensor(output_with_offload) else output_without_offload
)
max_diff = np.abs(to_np(output_with_offload) - to_np(output_without_offload)).max()
self.assertLess(max_diff, 1e-4, "XFormers attention should not affect the inference results")
def test_vae_slicing(self):
return super().test_vae_slicing(image_count=2)
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
components.pop("pag_applied_layers", None)
pipe_sd = AnimateDiffPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).frames[0, -3:, -3:, -1]
components = self.get_dummy_components()
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).frames[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).frames[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
components.pop("pag_applied_layers", None)
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
# pag_applied_layers = ["mid","up","down"] should apply to all self-attention layers
# Note that for motion modules in AnimateDiff, both attn1 and attn2 are self-attention
@@ -526,7 +179,7 @@ class AnimateDiffPAGPipelineFastTests(
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["mid_block.attentions.1"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
# pag_applied_layers = "down" should apply to all self-attention layers in down_blocks
@@ -551,13 +204,19 @@ class AnimateDiffPAGPipelineFastTests(
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["motion_modules.42"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"num_images_per_prompt": 1,
"do_classifier_free_guidance": self.get_dummy_inputs(device=torch_device).get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict)
class TestAnimateDiffPAGPipelineMemory(AnimateDiffPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the AnimateDiff PAG pipeline."""
class TestAnimateDiffPAGPipelineIPAdapter(AnimateDiffPAGPipelineTesterConfig, IPAdapterTesterMixin):
"""IP-Adapter tests for the AnimateDiff PAG pipeline."""
class TestAnimateDiffPAGPipelineFromPipe(AnimateDiffPAGPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline`."""
original_pipeline_repo = "hf-internal-testing/tinier-stable-diffusion-pipe"

View File

@@ -13,10 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import unittest
import numpy as np
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -30,37 +26,30 @@ from diffusers import (
)
from diffusers.utils.torch_utils import randn_tensor
from ...testing_utils import enable_full_determinism, torch_device
from ...testing_utils import assert_tensors_close, enable_full_determinism, torch_device
from ..pipeline_params import (
TEXT_TO_IMAGE_BATCH_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
TEXT_TO_IMAGE_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionControlNetPAGPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionControlNetPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionControlNetPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
output_shape = (3, 64, 64)
def get_dummy_components(self, time_cond_proj_dim=None):
# Copied from tests.pipelines.controlnet.test_controlnet_sdxl.StableDiffusionXLControlNetPipelineFastTests.get_dummy_components
@@ -120,7 +109,7 @@ class StableDiffusionControlNetPAGPipelineFastTests(
text_encoder = CLIPTextModel(text_encoder_config)
tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
components = {
return {
"unet": unet,
"controlnet": controlnet,
"scheduler": scheduler,
@@ -131,123 +120,88 @@ class StableDiffusionControlNetPAGPipelineFastTests(
"feature_extractor": None,
"image_encoder": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
def get_dummy_inputs(self):
generator = self.get_generator(0)
controlnet_embedder_scale_factor = 2
# The conditioning image is drawn from the same generator, which is then handed to the pipeline in the
# state that leaves it — the expected slices below were recorded that way.
image = randn_tensor(
(1, 3, 32 * controlnet_embedder_scale_factor, 32 * controlnet_embedder_scale_factor),
generator=generator,
device=torch.device(device),
device=torch.device("cpu"),
)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"num_inference_steps": 2,
"guidance_scale": 6.0,
"pag_scale": 3.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"image": image,
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusionControlNetPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestStableDiffusionControlNetPAGPipeline(
StableDiffusionControlNetPAGPipelineTesterConfig, PAGPipelineTesterMixin
):
base_pipeline_class = StableDiffusionControlNetPipeline
def test_pag_cfg(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.4533307, 0.28746086, 0.16543725, 0.786471, 0.5409842, 0.40167668, 0.7050086, 0.69445, 0.42238155]
image = pipe_pag(**self.get_dummy_inputs())[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.4533307, 0.28746086, 0.16543725, 0.786471, 0.5409842, 0.40167668, 0.7050086, 0.69445, 0.42238155])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_pag_uncond(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs["guidance_scale"] = 0.0
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.45054412, 0.27958393, 0.15983358, 0.80098593, 0.5432001, 0.4018666, 0.7127423, 0.69932824, 0.42260933]
image = pipe_pag(**inputs)[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.45054412, 0.27958393, 0.15983358, 0.80098593, 0.5432001, 0.4018666, 0.7127423, 0.69932824, 0.42260933])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"do_classifier_free_guidance": self.get_dummy_inputs(device=torch_device).get("guidance_scale", 1.0) > 1.0,
"do_classifier_free_guidance": self.get_dummy_inputs().get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict)
class TestStableDiffusionControlNetPAGPipelineMemory(
StableDiffusionControlNetPAGPipelineTesterConfig, MemoryTesterMixin
):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SD ControlNet PAG pipeline."""
class TestStableDiffusionControlNetPAGPipelineIPAdapter(
StableDiffusionControlNetPAGPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SD ControlNet PAG pipeline."""
class TestStableDiffusionControlNetPAGPipelineFromPipe(
StableDiffusionControlNetPAGPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline`."""

View File

@@ -15,9 +15,7 @@
# This model implementation is heavily based on:
import inspect
import random
import unittest
import numpy as np
import torch
@@ -34,26 +32,24 @@ from diffusers import (
)
from diffusers.utils.torch_utils import randn_tensor
from ...testing_utils import enable_full_determinism, floats_tensor, torch_device
from ...testing_utils import assert_tensors_close, enable_full_determinism, floats_tensor, torch_device
from ..pipeline_params import (
TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS,
TEXT_GUIDED_IMAGE_INPAINTING_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
)
from ..test_pipelines_common import PipelineKarrasSchedulerTesterMixin, PipelineLatentTesterMixin, PipelineTesterMixin
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionControlNetPAGInpaintPipelineFastTests(
PipelineLatentTesterMixin, PipelineKarrasSchedulerTesterMixin, PipelineTesterMixin, unittest.TestCase
):
class StableDiffusionControlNetPAGInpaintPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionControlNetPAGInpaintPipeline
params = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS
batch_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS
image_params = frozenset({"control_image"}) # skip `image` and `mask` for now, only test for control_image
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS
batch_input_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS
# The output resolution follows the 64x64 input image.
output_shape = (3, 64, 64)
def get_dummy_components(self):
# Copied from tests.pipelines.controlnet.test_controlnet_inpaint.ControlNetInpaintPipelineFastTests.get_dummy_components
@@ -109,7 +105,7 @@ class StableDiffusionControlNetPAGInpaintPipelineFastTests(
text_encoder = CLIPTextModel(text_encoder_config)
tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
components = {
return {
"unet": unet,
"controlnet": controlnet,
"scheduler": scheduler,
@@ -120,130 +116,83 @@ class StableDiffusionControlNetPAGInpaintPipelineFastTests(
"feature_extractor": None,
"image_encoder": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
def get_dummy_inputs(self):
generator = self.get_generator(0)
controlnet_embedder_scale_factor = 2
# The control image is drawn from the same generator, which is then handed to the pipeline in the state
# that leaves it — the expected slices below were recorded that way.
control_image = randn_tensor(
(1, 3, 32 * controlnet_embedder_scale_factor, 32 * controlnet_embedder_scale_factor),
generator=generator,
device=torch.device(device),
device=torch.device("cpu"),
)
init_image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device)
init_image = floats_tensor((1, 3, 32, 32), rng=random.Random(0)).to(torch_device)
init_image = init_image.cpu().permute(0, 2, 3, 1)[0]
image = Image.fromarray(np.uint8(init_image)).convert("RGB").resize((64, 64))
mask_image = Image.fromarray(np.uint8(init_image + 4)).convert("RGB").resize((64, 64))
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"num_inference_steps": 2,
"guidance_scale": 6.0,
"pag_scale": 3.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"image": image,
"mask_image": mask_image,
"control_image": control_image,
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusionControlNetInpaintPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__calss__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestStableDiffusionControlNetPAGInpaintPipeline(
StableDiffusionControlNetPAGInpaintPipelineTesterConfig, PAGPipelineTesterMixin
):
base_pipeline_class = StableDiffusionControlNetInpaintPipeline
def test_pag_cfg(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.7277897, 0.61666954, 0.54722667, 0.595576, 0.593909, 0.56389576, 0.41761285, 0.50566983, 0.49766505]
image = pipe_pag(**self.get_dummy_inputs())[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.7277897, 0.61666954, 0.54722667, 0.595576, 0.593909, 0.56389576, 0.41761285, 0.50566983, 0.49766505])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_pag_uncond(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs["guidance_scale"] = 0.0
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.7349223, 0.60567534, 0.5428778, 0.6091342, 0.60273147, 0.57611704, 0.42401767, 0.5064247, 0.49535546]
image = pipe_pag(**inputs)[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.7349223, 0.60567534, 0.5428778, 0.6091342, 0.60273147, 0.57611704, 0.42401767, 0.5064247, 0.49535546])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"do_classifier_free_guidance": self.get_dummy_inputs(device=torch_device).get("guidance_scale", 1.0) > 1.0,
"do_classifier_free_guidance": self.get_dummy_inputs().get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict)
class TestStableDiffusionControlNetPAGInpaintPipelineMemory(
StableDiffusionControlNetPAGInpaintPipelineTesterConfig, MemoryTesterMixin
):
"""Memory tests (CPU offload, group offload, layerwise casting) for the SD ControlNet PAG inpaint pipeline."""

View File

@@ -13,10 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import unittest
import numpy as np
import pytest
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTextModelWithProjection, CLIPTokenizer
@@ -30,37 +27,30 @@ from diffusers import (
)
from diffusers.utils.torch_utils import randn_tensor
from ...testing_utils import enable_full_determinism
from ...testing_utils import assert_tensors_close, enable_full_determinism
from ..pipeline_params import (
TEXT_TO_IMAGE_BATCH_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
TEXT_TO_IMAGE_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionXLControlNetPAGPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionXLControlNetPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionXLControlNetPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
output_shape = (3, 64, 64)
def get_dummy_components(self, time_cond_proj_dim=None):
# Copied from tests.pipelines.controlnet.test_controlnet_sdxl.StableDiffusionXLControlNetPipelineFastTests.get_dummy_components
@@ -151,114 +141,84 @@ class StableDiffusionXLControlNetPAGPipelineFastTests(
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
def get_dummy_inputs(self):
generator = self.get_generator(0)
controlnet_embedder_scale_factor = 2
# The conditioning image is drawn from the same generator, which is then handed to the pipeline in the
# state that leaves it — the expected slices below were recorded that way.
image = randn_tensor(
(1, 3, 32 * controlnet_embedder_scale_factor, 32 * controlnet_embedder_scale_factor),
generator=generator,
device=torch.device(device),
device=torch.device("cpu"),
)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"num_inference_steps": 2,
"guidance_scale": 6.0,
"pag_scale": 3.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"image": image,
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusionXLControlNetPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestStableDiffusionXLControlNetPAGPipeline(
StableDiffusionXLControlNetPAGPipelineTesterConfig, PAGPipelineTesterMixin
):
base_pipeline_class = StableDiffusionXLControlNetPipeline
def test_pag_cfg(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
image = pipe_pag(**self.get_dummy_inputs())[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array([0.6864, 0.5436, 0.5644, 0.6136, 0.5541, 0.5910, 0.4519, 0.4634, 0.5252])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.6864, 0.5436, 0.5644, 0.6136, 0.5541, 0.5910, 0.4519, 0.4634, 0.5252])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_pag_uncond(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs["guidance_scale"] = 0.0
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
image = pipe_pag(**inputs)[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array([0.6843, 0.5381, 0.5675, 0.6109, 0.5493, 0.5988, 0.4477, 0.4679, 0.5242])
# fmt: off
expected_slice = torch.tensor([0.6843, 0.5381, 0.5675, 0.6109, 0.5493, 0.5988, 0.4477, 0.4679, 0.5242])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
@unittest.skip("We test this functionality elsewhere already.")
@pytest.mark.skip("We test this functionality elsewhere already.")
def test_save_load_optional_components(self):
pass
class TestStableDiffusionXLControlNetPAGPipelineMemory(
StableDiffusionXLControlNetPAGPipelineTesterConfig, MemoryTesterMixin
):
"""Memory tests (CPU offload, group offload, layerwise casting) for the SDXL ControlNet PAG pipeline."""
class TestStableDiffusionXLControlNetPAGPipelineIPAdapter(
StableDiffusionXLControlNetPAGPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SDXL ControlNet PAG pipeline."""
class TestStableDiffusionXLControlNetPAGPipelineFromPipe(
StableDiffusionXLControlNetPAGPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` round-trip tests against `StableDiffusionXLPipeline`."""

View File

@@ -13,11 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import random
import unittest
import numpy as np
import pytest
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTextModelWithProjection, CLIPTokenizer
@@ -30,38 +28,34 @@ from diffusers import (
UNet2DConditionModel,
)
from ...testing_utils import enable_full_determinism, floats_tensor
from ...testing_utils import assert_tensors_close, enable_full_determinism, floats_tensor, torch_device
from ..pipeline_params import (
IMAGE_TO_IMAGE_IMAGE_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionXLControlNetPAGImg2ImgPipelineFastTests(
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionXLControlNetPAGImg2ImgPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionXLControlNetPAGImg2ImgPipeline
params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
image_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
image_latents_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS.union(
{"pag_scale", "pag_adaptive_scale"}
)
batch_input_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union(
{"add_text_embeds", "add_time_ids", "add_neg_time_ids"}
)
# The output resolution follows the 64x64 conditioning image.
output_shape = (3, 64, 64)
# Copied from tests.pipelines.controlnet.test_controlnet_sdxl_img2img.ControlNetPipelineSDXLImg2ImgFastTests.get_dummy_components
def get_dummy_components(self, skip_first_text_encoder=False):
@@ -153,117 +147,80 @@ class StableDiffusionXLControlNetPAGImg2ImgPipelineFastTests(
# based on tests.pipelines.controlnet.test_controlnet_sdxl_img2img.ControlNetPipelineSDXLImg2ImgFastTests.get_dummy_inputs
# add `pag_scale` to the inputs
def get_dummy_inputs(self, device, seed=0):
def get_dummy_inputs(self):
controlnet_embedder_scale_factor = 2
image = floats_tensor(
(1, 3, 32 * controlnet_embedder_scale_factor, 32 * controlnet_embedder_scale_factor),
rng=random.Random(seed),
).to(device)
rng=random.Random(0),
).to(torch_device)
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 6.0,
"pag_scale": 3.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"image": image,
"control_image": image,
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe_sd = StableDiffusionXLControlNetImg2ImgPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enable
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestStableDiffusionXLControlNetPAGImg2ImgPipeline(
StableDiffusionXLControlNetPAGImg2ImgPipelineTesterConfig, PAGPipelineTesterMixin
):
base_pipeline_class = StableDiffusionXLControlNetImg2ImgPipeline
@pytest.mark.skip("We test this functionality elsewhere already.")
def test_save_load_optional_components(self):
pass
def test_pag_cfg(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.55155665, 0.4650753, 0.46541628, 0.60965055, 0.55995595, 0.49751496, 0.5937391, 0.5700847, 0.44238678]
image = pipe_pag(**self.get_dummy_inputs())[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.55155665, 0.4650753, 0.46541628, 0.60965055, 0.55995595, 0.49751496, 0.5937391, 0.5700847, 0.44238678])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_pag_uncond(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# Run on CPU: the expected slice below is CPU-specific.
pipe_pag = self.get_pag_pipeline(pag_applied_layers=["mid", "up", "down"])
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs["guidance_scale"] = 0.0
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.549061, 0.46218234, 0.4675981, 0.6109464, 0.5547849, 0.4960261, 0.60211027, 0.5698843, 0.44092298]
image = pipe_pag(**inputs)[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
# fmt: off
expected_slice = torch.tensor([0.549061, 0.46218234, 0.4675981, 0.6109464, 0.5547849, 0.4960261, 0.60211027, 0.5698843, 0.44092298])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
class TestStableDiffusionXLControlNetPAGImg2ImgPipelineMemory(
StableDiffusionXLControlNetPAGImg2ImgPipelineTesterConfig, MemoryTesterMixin
):
"""Memory tests (CPU offload, group offload, layerwise casting) for the SDXL ControlNet PAG img2img pipeline."""
class TestStableDiffusionXLControlNetPAGImg2ImgPipelineIPAdapter(
StableDiffusionXLControlNetPAGImg2ImgPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SDXL ControlNet PAG img2img pipeline."""
class TestStableDiffusionXLControlNetPAGImg2ImgPipelineFromPipe(
StableDiffusionXLControlNetPAGImg2ImgPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` round-trip tests against `StableDiffusionXLPipeline`."""

View File

@@ -13,11 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import tempfile
import unittest
import numpy as np
import pytest
import torch
from transformers import AutoConfig, AutoTokenizer, BertModel, T5EncoderModel
@@ -29,22 +25,21 @@ from diffusers import (
HunyuanDiTPipeline,
)
from ...testing_utils import enable_full_determinism, torch_device
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_IMAGE_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineTesterMixin, to_np
from ...testing_utils import assert_tensors_close, enable_full_determinism, torch_device
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class HunyuanDiTPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
class HunyuanDiTPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = HunyuanDiTPAGPipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params = PipelineTesterMixin.required_optional_params
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
# `transformer.sample_size` (16) * `vae_scale_factor` (8) / 8 -> the dummy transformer generates 16x16 images.
output_shape = (3, 16, 16)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -72,7 +67,7 @@ class HunyuanDiTPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
text_encoder_2 = T5EncoderModel(config)
tokenizer_2 = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5")
components = {
return {
"transformer": transformer.eval(),
"vae": vae.eval(),
"scheduler": scheduler,
@@ -83,159 +78,88 @@ class HunyuanDiTPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
"safety_checker": None,
"feature_extractor": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"use_resolution_binning": False,
"pag_scale": 0.0,
}
return inputs
class TestHunyuanDiTPAGPipeline(HunyuanDiTPAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = HunyuanDiTPipeline
# HunyuanDiT's denoiser is a transformer: PAG uses the pipeline's default applied layers, turned on by raising
# `pag_scale` (the dummy inputs disable it with `pag_scale=0.0`).
pag_enabled_applied_layers = None
pag_enabled_scale = 3.0
def test_inference(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs()).images
assert image.shape == (1, *self.output_shape)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice = image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (1, 16, 16, 3))
expected_slice = np.array(
[0.56939435, 0.34541583, 0.35915792, 0.46489206, 0.38775963, 0.45004836, 0.5957267, 0.59481275, 0.33287364]
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
@unittest.skip("Not supported.")
def test_sequential_cpu_offload_forward_pass(self):
# TODO(YiYi) need to fix later
pass
@unittest.skip("Not supported.")
def test_sequential_offload_forward_pass_twice(self):
# TODO(YiYi) need to fix later
pass
# fmt: off
expected_slice = torch.tensor([0.56939435, 0.34541583, 0.35915792, 0.46489206, 0.38775963, 0.45004836, 0.5957267, 0.59481275, 0.33287364])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_inference_batch_single_identical(self):
self._test_inference_batch_single_identical(
expected_max_diff=1e-3,
)
super().test_inference_batch_single_identical(expected_max_diff=1e-3)
def test_feed_forward_chunking(self):
device = "cpu"
# Run on CPU to keep the device-dependent `torch.Generator` deterministic.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice_no_chunking = image[0, -3:, -3:, -1]
output_no_chunking = self.run_pipe(pipe)
pipe.transformer.enable_forward_chunking(chunk_size=1, dim=0)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice_chunking = image[0, -3:, -3:, -1]
output_chunking = self.run_pipe(pipe)
max_diff = np.abs(to_np(image_slice_no_chunking) - to_np(image_slice_chunking)).max()
self.assertLess(max_diff, 1e-4)
assert_tensors_close(
output_chunking, output_no_chunking, atol=1e-4, msg="Forward chunking changed the output."
)
def test_fused_qkv_projections(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
# Run on CPU to keep the device-dependent `torch.Generator` deterministic.
pipe = self.get_pipeline()
inputs = self.get_dummy_inputs(device)
inputs["return_dict"] = False
image = pipe(**inputs)[0]
original_image_slice = image[0, -3:, -3:, -1]
original_output = self.run_pipe(pipe)
pipe.transformer.fuse_qkv_projections()
inputs = self.get_dummy_inputs(device)
inputs["return_dict"] = False
image_fused = pipe(**inputs)[0]
image_slice_fused = image_fused[0, -3:, -3:, -1]
output_fused = self.run_pipe(pipe)
pipe.transformer.unfuse_qkv_projections()
inputs = self.get_dummy_inputs(device)
inputs["return_dict"] = False
image_disabled = pipe(**inputs)[0]
image_slice_disabled = image_disabled[0, -3:, -3:, -1]
output_disabled = self.run_pipe(pipe)
assert np.allclose(original_image_slice, image_slice_fused, atol=1e-2, rtol=1e-2), (
"Fusion of QKV projections shouldn't affect the outputs."
assert_tensors_close(
output_fused, original_output, atol=1e-2, rtol=1e-2, msg="Fusion of QKV projections changed the outputs."
)
assert np.allclose(image_slice_fused, image_slice_disabled, atol=1e-2, rtol=1e-2), (
"Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled."
assert_tensors_close(
output_disabled,
output_fused,
atol=1e-2,
rtol=1e-2,
msg="Outputs changed after the fused QKV projections were disabled.",
)
assert np.allclose(original_image_slice, image_slice_disabled, atol=1e-2, rtol=1e-2), (
"Original outputs should match when fused QKV projections are disabled."
assert_tensors_close(
output_disabled,
original_output,
atol=1e-2,
rtol=1e-2,
msg="Original outputs should match when fused QKV projections are disabled.",
)
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = HunyuanDiTPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
components = self.get_dummy_components()
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 3.0
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
all_self_attn_layers = [k for k in pipe.transformer.attn_processors.keys() if "attn1" in k]
original_attn_procs = pipe.transformer.attn_processors
@@ -265,19 +189,16 @@ class HunyuanDiTPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert len(pipe.pag_attn_processors) == 2
@unittest.skip(
@pytest.mark.skip(
"Test not supported as `encode_prompt` is called two times separately which deivates from about 99% of the pipelines we have."
)
def test_encode_prompt_works_in_isolation(self):
pass
def test_save_load_optional_components(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
def test_save_load_optional_components(self, tmp_path, expected_max_difference=1e-4):
pipe = self.get_pipeline().to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
inputs = self.get_dummy_inputs()
prompt = inputs["prompt"]
generator = inputs["generator"]
@@ -325,19 +246,17 @@ class HunyuanDiTPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
output = pipe(**inputs)[0]
with tempfile.TemporaryDirectory() as tmpdir:
pipe.save_pretrained(tmpdir)
pipe_loaded = self.pipeline_class.from_pretrained(tmpdir)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
pipe.save_pretrained(tmp_path)
pipe_loaded = self.pipeline_class.from_pretrained(tmp_path)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
for optional_component in pipe._optional_components:
self.assertTrue(
getattr(pipe_loaded, optional_component) is None,
f"`{optional_component}` did not stay set to None after loading.",
assert getattr(pipe_loaded, optional_component) is None, (
f"`{optional_component}` did not stay set to None after loading."
)
inputs = self.get_dummy_inputs(torch_device)
inputs = self.get_dummy_inputs()
generator = inputs["generator"]
num_inference_steps = inputs["num_inference_steps"]
@@ -361,5 +280,23 @@ class HunyuanDiTPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
output_loaded = pipe_loaded(**inputs)[0]
max_diff = np.abs(to_np(output) - to_np(output_loaded)).max()
self.assertLess(max_diff, 1e-4)
assert_tensors_close(
output_loaded,
output,
atol=expected_max_difference,
msg="Output changed after dropping optional components.",
)
class TestHunyuanDiTPAGPipelineMemory(HunyuanDiTPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the HunyuanDiT PAG pipeline."""
@pytest.mark.skip("Not supported.")
def test_sequential_cpu_offload_forward_pass(self):
# TODO(YiYi) need to fix later
pass
@pytest.mark.skip("Not supported.")
def test_sequential_offload_forward_pass_twice(self):
# TODO(YiYi) need to fix later
pass

View File

@@ -13,10 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import unittest
import numpy as np
import pytest
import torch
from diffusers import (
@@ -32,29 +29,21 @@ from ...testing_utils import enable_full_determinism
from ..pipeline_params import (
TEXT_TO_IMAGE_BATCH_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
TEXT_TO_IMAGE_PARAMS,
)
from ..test_pipelines_common import (
PipelineFromPipeTesterMixin,
PipelineTesterMixin,
)
from ..testing_utils import BasePipelineTesterConfig, FromPipeTesterMixin, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class KolorsPAGPipelineFastTests(
PipelineTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class KolorsPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = KolorsPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
output_shape = (3, 64, 64)
# Copied from tests.pipelines.kolors.test_kolors.KolorsPipelineTesterConfig.get_dummy_components
def get_dummy_components(self, time_cond_proj_dim=None):
@@ -112,65 +101,27 @@ class KolorsPAGPipelineFastTests(
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"pag_scale": 0.9,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = KolorsPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestKolorsPAGPipeline(KolorsPAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = KolorsPipeline
# fmt: off
expected_pag_slice = torch.tensor([0.26030684, 0.43192005, 0.4042826, 0.4189067, 0.5181305, 0.3832534, 0.472135, 0.4145031, 0.43726248])
# fmt: on
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
# pag_applied_layers = ["mid","up","down"] should apply to all self-attention layers
all_self_attn_layers = [k for k in pipe.unet.attn_processors.keys() if "attn1" in k]
@@ -201,7 +152,7 @@ class KolorsPAGPipelineFastTests(
# pag_applied_layers = ["mid.block_0.attentions_1"] does not exist in the model
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["mid_block.attentions.1"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
# pag_applied_layers = "down" should apply to all self-attention layers in down_blocks
@@ -212,7 +163,7 @@ class KolorsPAGPipelineFastTests(
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["down_blocks.0"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
pipe.unet.set_attn_processor(original_attn_procs.copy())
@@ -225,33 +176,16 @@ class KolorsPAGPipelineFastTests(
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert len(pipe.pag_attn_processors) == 2
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.26030684, 0.43192005, 0.4042826, 0.4189067, 0.5181305, 0.3832534, 0.472135, 0.4145031, 0.43726248]
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
def test_inference_batch_single_identical(self):
self._test_inference_batch_single_identical(expected_max_diff=3e-3)
super().test_inference_batch_single_identical(expected_max_diff=3e-3)
def test_encode_prompt_works_in_isolation(self):
return super().test_encode_prompt_works_in_isolation(atol=1e-3, rtol=1e-3)
class TestKolorsPAGPipelineMemory(KolorsPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the Kolors PAG pipeline."""
class TestKolorsPAGPipelineFromPipe(KolorsPAGPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `KolorsPipeline`."""

View File

@@ -13,15 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import tempfile
import unittest
import numpy as np
import pytest
import torch
from transformers import AutoConfig, AutoTokenizer, T5EncoderModel
import diffusers
from diffusers import (
AutoencoderKL,
DDIMScheduler,
@@ -29,33 +24,30 @@ from diffusers import (
PixArtSigmaPipeline,
PixArtTransformer2DModel,
)
from diffusers.utils import logging
from ...testing_utils import (
CaptureLogger,
assert_tensors_close,
enable_full_determinism,
torch_device,
)
from ..pipeline_params import (
TEXT_TO_IMAGE_BATCH_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
TEXT_TO_IMAGE_PARAMS,
)
from ..test_pipelines_common import PipelineTesterMixin, assert_mean_pixel_difference, to_np
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class PixArtSigmaPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
class PixArtSigmaPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = PixArtSigmaPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
params = set(params)
params.remove("cross_attention_kwargs")
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params = PipelineTesterMixin.required_optional_params
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"}) - {
"cross_attention_kwargs"
}
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
# `transformer.sample_size` (8) * `vae_scale_factor` (8) / 8 -> the dummy transformer generates 8x8 images.
output_shape = (3, 8, 8)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -85,76 +77,40 @@ class PixArtSigmaPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5")
components = {
return {
"transformer": transformer.eval(),
"vae": vae.eval(),
"scheduler": scheduler,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 1.0,
"pag_scale": 3.0,
"use_resolution_binning": False,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe = PixArtSigmaPipeline(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe.__class__.__name__}."
)
out = pipe(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
components["pag_applied_layers"] = ["blocks.1"]
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestPixArtSigmaPAGPipeline(PixArtSigmaPAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = PixArtSigmaPipeline
# PixArt's denoiser is a transformer, so PAG resolves per transformer block rather than the UNet's mid/up/down.
pag_enabled_applied_layers = ["blocks.1"]
# `test_pag_inference` builds the pipeline with the class default (`blocks.1`), as it did before the migration.
pag_inference_applied_layers = None
# fmt: off
expected_pag_slice = torch.tensor([0.6499, 0.3250, 0.3572, 0.6780, 0.4453, 0.4582, 0.2770, 0.5168, 0.4594])
# fmt: on
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
# "attn1" should apply to all self-attention layers.
all_self_attn_layers = [k for k in pipe.transformer.attn_processors.keys() if "attn1" in k]
@@ -162,189 +118,38 @@ class PixArtSigmaPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert set(pipe.pag_attn_processors) == set(all_self_attn_layers)
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
def test_attention_slicing_forward_pass(self, expected_max_diff=1e-3):
# Run on CPU: sliced attention is compared against a full-attention run of the same pipeline.
pipe = self.get_pipeline()
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
8,
8,
3,
), f"the shape of the output image should be (1, 8, 8, 3) but got {image.shape}"
expected_slice = np.array([0.6499, 0.3250, 0.3572, 0.6780, 0.4453, 0.4582, 0.2770, 0.5168, 0.4594])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
# Because the PAG PixArt Sigma has `pag_applied_layers`.
# Also, we shouldn't be doing `set_default_attn_processor()` after loading
# the pipeline with `pag_applied_layers`.
def test_save_load_local(self, expected_max_difference=1e-4):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(torch_device)
output = pipe(**inputs)[0]
logger = logging.get_logger("diffusers.pipelines.pipeline_utils")
logger.setLevel(diffusers.logging.INFO)
with tempfile.TemporaryDirectory() as tmpdir:
pipe.save_pretrained(tmpdir, safe_serialization=False)
with CaptureLogger(logger) as cap_logger:
pipe_loaded = self.pipeline_class.from_pretrained(tmpdir, pag_applied_layers=["blocks.1"])
for name in pipe_loaded.components.keys():
if name not in pipe_loaded._optional_components:
assert name in str(cap_logger)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(torch_device)
output_loaded = pipe_loaded(**inputs)[0]
max_diff = np.abs(to_np(output) - to_np(output_loaded)).max()
self.assertLess(max_diff, expected_max_difference)
# We shouldn't be setting `set_default_attn_processor` here.
def test_attention_slicing_forward_pass(
self, test_max_difference=True, test_mean_pixel_difference=True, expected_max_diff=1e-3
):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
generator_device = "cpu"
inputs = self.get_dummy_inputs(generator_device)
output_without_slicing = pipe(**inputs)[0]
output_without_slicing = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=1)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing1 = pipe(**inputs)[0]
output_with_slicing_1 = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=2)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing2 = pipe(**inputs)[0]
output_with_slicing_2 = self.run_pipe(pipe)
if test_max_difference:
max_diff1 = np.abs(to_np(output_with_slicing1) - to_np(output_without_slicing)).max()
max_diff2 = np.abs(to_np(output_with_slicing2) - to_np(output_without_slicing)).max()
self.assertLess(
max(max_diff1, max_diff2),
expected_max_diff,
"Attention slicing should not affect the inference results",
)
assert_tensors_close(
output_with_slicing_1,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=1) changed the output.",
)
assert_tensors_close(
output_with_slicing_2,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=2) changed the output.",
)
if test_mean_pixel_difference:
assert_mean_pixel_difference(to_np(output_with_slicing1[0]), to_np(output_without_slicing[0]))
assert_mean_pixel_difference(to_np(output_with_slicing2[0]), to_np(output_without_slicing[0]))
def test_inference_batch_single_identical(self):
super().test_inference_batch_single_identical(batch_size=2)
# Because we have `pag_applied_layers` we cannot directly apply
# `set_default_attn_processor`
def test_dict_tuple_outputs_equivalent(self, expected_slice=None, expected_max_difference=1e-4):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
generator_device = "cpu"
if expected_slice is None:
output = pipe(**self.get_dummy_inputs(generator_device))[0]
else:
output = expected_slice
output_tuple = pipe(**self.get_dummy_inputs(generator_device), return_dict=False)[0]
if expected_slice is None:
max_diff = np.abs(to_np(output) - to_np(output_tuple)).max()
else:
if output_tuple.ndim != 5:
max_diff = np.abs(to_np(output) - to_np(output_tuple)[0, -3:, -3:, -1].flatten()).max()
else:
max_diff = np.abs(to_np(output) - to_np(output_tuple)[0, -3:, -3:, -1, -1].flatten()).max()
self.assertLess(max_diff, expected_max_difference)
# Same reason as above
def test_inference_batch_single_identical(
self,
batch_size=2,
expected_max_diff=1e-4,
additional_params_copy_to_batched_inputs=["num_inference_steps"],
):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(torch_device)
# Reset generator in case it is has been used in self.get_dummy_inputs
inputs["generator"] = self.get_generator(0)
logger = logging.get_logger(pipe.__module__)
logger.setLevel(level=diffusers.logging.FATAL)
# batchify inputs
batched_inputs = {}
batched_inputs.update(inputs)
for name in self.batch_params:
if name not in inputs:
continue
value = inputs[name]
if name == "prompt":
len_prompt = len(value)
batched_inputs[name] = [value[: len_prompt // i] for i in range(1, batch_size + 1)]
batched_inputs[name][-1] = 100 * "very long"
else:
batched_inputs[name] = batch_size * [value]
if "generator" in inputs:
batched_inputs["generator"] = [self.get_generator(i) for i in range(batch_size)]
if "batch_size" in inputs:
batched_inputs["batch_size"] = batch_size
for arg in additional_params_copy_to_batched_inputs:
batched_inputs[arg] = inputs[arg]
output = pipe(**inputs)
output_batch = pipe(**batched_inputs)
assert output_batch[0].shape[0] == batch_size
max_diff = np.abs(to_np(output_batch[0][0]) - to_np(output[0][0])).max()
assert max_diff < expected_max_diff
# Because we're passing `pag_applied_layers` (type of List) in the components as well.
def test_components_function(self):
init_components = self.get_dummy_components()
init_components = {k: v for k, v in init_components.items() if not isinstance(v, (str, int, float, list))}
pipe = self.pipeline_class(**init_components)
self.assertTrue(hasattr(pipe, "components"))
self.assertTrue(set(pipe.components.keys()) == set(init_components.keys()))
@unittest.skip("Test is already covered through encode_prompt isolation.")
@pytest.mark.skip("Test is already covered through encode_prompt isolation.")
def test_save_load_optional_components(self):
pass
class TestPixArtSigmaPAGPipelineMemory(PixArtSigmaPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the PixArt-sigma PAG pipeline."""

View File

@@ -12,10 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import unittest
import numpy as np
import pytest
import torch
from transformers import Gemma2Config, Gemma2ForCausalLM, GemmaTokenizer
@@ -27,31 +24,26 @@ from diffusers import (
SanaTransformer2DModel,
)
from ...testing_utils import enable_full_determinism, torch_device
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_IMAGE_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineTesterMixin, to_np
from ...testing_utils import (
assert_tensors_close,
enable_full_determinism,
require_accelerator,
skip_if_no_cudnn_engine,
torch_device,
)
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class SanaPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
class SanaPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = SanaPAGPipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params = frozenset(
[
"num_inference_steps",
"generator",
"latents",
"return_dict",
"callback_on_step_end",
"callback_on_step_end_tensor_inputs",
]
)
test_xformers_attention = False
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
output_shape = (3, 32, 32)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -115,183 +107,64 @@ class SanaPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
text_encoder = Gemma2ForCausalLM(text_encoder_config)
tokenizer = GemmaTokenizer.from_pretrained("hf-internal-testing/dummy-gemma")
components = {
return {
"transformer": transformer,
"vae": vae,
"scheduler": scheduler,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "",
"negative_prompt": "",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 6.0,
"pag_scale": 3.0,
"height": 32,
"width": 32,
"max_sequence_length": 16,
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"complex_human_instruction": None,
}
return inputs
def test_inference(self):
device = "cpu"
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
class TestSanaPAGPipeline(SanaPAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = SanaPipeline
# Only the "PAG off reproduces the base pipeline" leg was asserted before the migration.
check_pag_changes_output = False
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs)[0]
generated_image = image[0]
def test_attention_slicing_forward_pass(self, expected_max_diff=1e-3):
# Run on CPU: sliced attention is compared against a full-attention run of the same pipeline.
pipe = self.get_pipeline()
self.assertEqual(generated_image.shape, (3, 32, 32))
expected_image = torch.randn(3, 32, 32)
max_diff = np.abs(generated_image - expected_image).max()
self.assertLessEqual(max_diff, 1e10)
def test_callback_inputs(self):
sig = inspect.signature(self.pipeline_class.__call__)
has_callback_tensor_inputs = "callback_on_step_end_tensor_inputs" in sig.parameters
has_callback_step_end = "callback_on_step_end" in sig.parameters
if not (has_callback_tensor_inputs and has_callback_step_end):
return
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe = pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
self.assertTrue(
hasattr(pipe, "_callback_tensor_inputs"),
f" {self.pipeline_class} should have `_callback_tensor_inputs` that defines a list of tensor variables its callback function can use as inputs",
)
def callback_inputs_subset(pipe, i, t, callback_kwargs):
# iterate over callback args
for tensor_name, tensor_value in callback_kwargs.items():
# check that we're only passing in allowed tensor inputs
assert tensor_name in pipe._callback_tensor_inputs
return callback_kwargs
def callback_inputs_all(pipe, i, t, callback_kwargs):
for tensor_name in pipe._callback_tensor_inputs:
assert tensor_name in callback_kwargs
# iterate over callback args
for tensor_name, tensor_value in callback_kwargs.items():
# check that we're only passing in allowed tensor inputs
assert tensor_name in pipe._callback_tensor_inputs
return callback_kwargs
inputs = self.get_dummy_inputs(torch_device)
# Test passing in a subset
inputs["callback_on_step_end"] = callback_inputs_subset
inputs["callback_on_step_end_tensor_inputs"] = ["latents"]
output = pipe(**inputs)[0]
# Test passing in a everything
inputs["callback_on_step_end"] = callback_inputs_all
inputs["callback_on_step_end_tensor_inputs"] = pipe._callback_tensor_inputs
output = pipe(**inputs)[0]
def callback_inputs_change_tensor(pipe, i, t, callback_kwargs):
is_last = i == (pipe.num_timesteps - 1)
if is_last:
callback_kwargs["latents"] = torch.zeros_like(callback_kwargs["latents"])
return callback_kwargs
inputs["callback_on_step_end"] = callback_inputs_change_tensor
inputs["callback_on_step_end_tensor_inputs"] = pipe._callback_tensor_inputs
output = pipe(**inputs)[0]
assert output.abs().sum() < 1e10
def test_attention_slicing_forward_pass(
self, test_max_difference=True, test_mean_pixel_difference=True, expected_max_diff=1e-3
):
if not self.test_attention_slicing:
return
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
for component in pipe.components.values():
if hasattr(component, "set_default_attn_processor"):
component.set_default_attn_processor()
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
generator_device = "cpu"
inputs = self.get_dummy_inputs(generator_device)
output_without_slicing = pipe(**inputs)[0]
output_without_slicing = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=1)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing1 = pipe(**inputs)[0]
output_with_slicing_1 = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=2)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing2 = pipe(**inputs)[0]
output_with_slicing_2 = self.run_pipe(pipe)
if test_max_difference:
max_diff1 = np.abs(to_np(output_with_slicing1) - to_np(output_without_slicing)).max()
max_diff2 = np.abs(to_np(output_with_slicing2) - to_np(output_without_slicing)).max()
self.assertLess(
max(max_diff1, max_diff2),
expected_max_diff,
"Attention slicing should not affect the inference results",
)
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = SanaPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
assert_tensors_close(
output_with_slicing_1,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=1) changed the output.",
)
assert_tensors_close(
output_with_slicing_2,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=2) changed the output.",
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
components = self.get_dummy_components()
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
all_self_attn_layers = [k for k in pipe.transformer.attn_processors.keys() if "attn1" in k]
original_attn_procs = pipe.transformer.attn_processors
@@ -322,18 +195,33 @@ class SanaPAGPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
assert len(pipe.pag_attn_processors) == 2
# TODO(aryan): Create a dummy gemma model with smol vocab size
@unittest.skip(
@pytest.mark.skip(
"A very small vocab size is used for fast tests. So, Any kind of prompt other than the empty default used in other tests will lead to a embedding lookup error. This test uses a long prompt that causes the error."
)
def test_inference_batch_consistent(self):
pass
@unittest.skip(
@pytest.mark.skip(
"A very small vocab size is used for fast tests. So, Any kind of prompt other than the empty default used in other tests will lead to a embedding lookup error. This test uses a long prompt that causes the error."
)
def test_inference_batch_single_identical(self):
pass
def test_float16_inference(self):
# Requires higher tolerance as model seems very sensitive to dtype
super().test_float16_inference(expected_max_diff=0.08)
# Sana's multiscale linear attention runs a depthwise `Conv2d`, which some cuDNN builds have no bfloat16
# engine for. The decorators below repeat the ones the base method is declared with: overriding a test drops
# the marks it inherited.
@pytest.mark.skipif(torch_device not in ["cuda", "xpu"], reason="half-precision inference requires CUDA or XPU")
@require_accelerator
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16], ids=str)
def test_half_precision_inference_no_nan(self, dtype):
with skip_if_no_cudnn_engine():
super().test_half_precision_inference_no_nan(dtype)
class TestSanaPAGPipelineMemory(SanaPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the Sana PAG pipeline."""
# Layerwise casting computes in bfloat16, which lands on the same missing depthwise-conv engine as above.
def test_layerwise_casting_inference(self):
with skip_if_no_cudnn_engine():
super().test_layerwise_casting_inference()

View File

@@ -14,10 +14,9 @@
# limitations under the License.
import gc
import inspect
import unittest
import numpy as np
import pytest
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -40,33 +39,26 @@ from ...testing_utils import (
from ..pipeline_params import (
TEXT_TO_IMAGE_BATCH_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
TEXT_TO_IMAGE_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionPAGPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
output_shape = (3, 64, 64)
def get_dummy_components(self, time_cond_proj_dim=None):
cross_attention_dim = 8
@@ -116,7 +108,7 @@ class StableDiffusionPAGPipelineFastTests(
text_encoder = CLIPTextModel(text_encoder_config)
tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
components = {
return {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
@@ -126,67 +118,28 @@ class StableDiffusionPAGPipelineFastTests(
"feature_extractor": None,
"image_encoder": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"pag_scale": 0.9,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusionPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestStableDiffusionPAGPipeline(StableDiffusionPAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusionPipeline
# fmt: off
expected_pag_slice = torch.tensor([0.23171297, 0.44669262, 0.48407662, 0.29981518, 0.36721927, 0.46788025, 0.46333545, 0.3314417, 0.42078137])
# fmt: on
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
# pag_applied_layers = ["mid","up","down"] should apply to all self-attention layers
all_self_attn_layers = [k for k in pipe.unet.attn_processors.keys() if "attn1" in k]
@@ -224,7 +177,7 @@ class StableDiffusionPAGPipelineFastTests(
# pag_applied_layers = ["mid.block_0.attentions_1"] does not exist in the model
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["mid_block.attentions.1"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
# pag_applied_layers = "down" should apply to all self-attention layers in down_blocks
@@ -239,7 +192,7 @@ class StableDiffusionPAGPipelineFastTests(
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["down_blocks.0"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
pipe.unet.set_attn_processor(original_attn_procs.copy())
@@ -252,52 +205,37 @@ class StableDiffusionPAGPipelineFastTests(
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert len(pipe.pag_attn_processors) == 1
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array(
[0.23171297, 0.44669262, 0.48407662, 0.29981518, 0.36721927, 0.46788025, 0.46333545, 0.3314417, 0.42078137]
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"do_classifier_free_guidance": self.get_dummy_inputs(device=torch_device).get("guidance_scale", 1.0) > 1.0,
"do_classifier_free_guidance": self.get_dummy_inputs().get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict)
class TestStableDiffusionPAGPipelineMemory(StableDiffusionPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SD PAG pipeline."""
class TestStableDiffusionPAGPipelineIPAdapter(StableDiffusionPAGPipelineTesterConfig, IPAdapterTesterMixin):
"""IP-Adapter tests for the SD PAG pipeline."""
class TestStableDiffusionPAGPipelineFromPipe(StableDiffusionPAGPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline`."""
@slow
@require_torch_accelerator
class StableDiffusionPAGPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusionPAGPipelineIntegration:
pipeline_class = StableDiffusionPAGPipeline
repo_id = "stable-diffusion-v1-5/stable-diffusion-v1-5"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -1,7 +1,3 @@
import inspect
import unittest
import numpy as np
import torch
from transformers import (
AutoConfig,
@@ -20,19 +16,19 @@ from diffusers import (
StableDiffusion3Pipeline,
)
from ...testing_utils import (
torch_device,
)
from ..test_pipelines_common import (
PipelineTesterMixin,
from ...testing_utils import assert_tensors_close, torch_device
from ..testing_utils import (
BasePipelineTesterConfig,
MemoryTesterMixin,
check_qkv_fusion_matches_attn_procs_length,
check_qkv_fusion_processors_exist,
)
from .testing_utils import PAGPipelineTesterMixin
class StableDiffusion3PAGPipelineFastTests(unittest.TestCase, PipelineTesterMixin):
class StableDiffusion3PAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusion3PAGPipeline
params = frozenset(
required_input_params_in_call_signature = frozenset(
[
"prompt",
"height",
@@ -43,8 +39,8 @@ class StableDiffusion3PAGPipelineFastTests(unittest.TestCase, PipelineTesterMixi
"negative_prompt_embeds",
]
)
batch_params = frozenset(["prompt", "negative_prompt"])
test_xformers_attention = False
batch_input_params = frozenset(["prompt", "negative_prompt"])
output_shape = (3, 32, 32)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -117,64 +113,50 @@ class StableDiffusion3PAGPipelineFastTests(unittest.TestCase, PipelineTesterMixi
"vae": vae,
}
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device="cpu").manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"pag_scale": 0.0,
}
return inputs
class TestStableDiffusion3PAGPipeline(StableDiffusion3PAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusion3Pipeline
# The dummy inputs disable PAG (`pag_scale=0.0`); only the "PAG off reproduces the base pipeline" leg is
# asserted here, as it was before the migration.
check_pag_changes_output = False
def test_stable_diffusion_3_different_prompts(self):
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
pipe = self.get_pipeline().to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
output_same_prompt = pipe(**inputs).images[0]
inputs = self.get_dummy_inputs(torch_device)
inputs["prompt_2"] = "a different prompt"
inputs["prompt_3"] = "another different prompt"
output_different_prompts = pipe(**inputs).images[0]
max_diff = np.abs(output_same_prompt - output_different_prompts).max()
output_same_prompt = self.run_pipe(pipe)[0]
output_different_prompts = self.run_pipe(
pipe, prompt_2="a different prompt", prompt_3="another different prompt"
)[0]
# Outputs should be different here
assert max_diff > 1e-2
assert (output_same_prompt - output_different_prompts).abs().max() > 1e-2
def test_stable_diffusion_3_different_negative_prompts(self):
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
pipe = self.get_pipeline().to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
output_same_prompt = pipe(**inputs).images[0]
inputs = self.get_dummy_inputs(torch_device)
inputs["negative_prompt_2"] = "deformed"
inputs["negative_prompt_3"] = "blurry"
output_different_prompts = pipe(**inputs).images[0]
max_diff = np.abs(output_same_prompt - output_different_prompts).max()
output_same_prompt = self.run_pipe(pipe)[0]
output_different_prompts = self.run_pipe(pipe, negative_prompt_2="deformed", negative_prompt_3="blurry")[0]
# Outputs should be different here
assert max_diff > 1e-2
assert (output_same_prompt - output_different_prompts).abs().max() > 1e-2
def test_fused_qkv_projections(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
# Run on CPU to keep the device-dependent `torch.Generator` deterministic.
pipe = self.get_pipeline()
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
original_image_slice = image[0, -3:, -3:, -1]
original_output = self.run_pipe(pipe)
# TODO (sayakpaul): will refactor this once `fuse_qkv_projections()` has been added
# to the pipeline level.
@@ -186,62 +168,31 @@ class StableDiffusion3PAGPipelineFastTests(unittest.TestCase, PipelineTesterMixi
pipe.transformer, pipe.transformer.original_attn_processors
), "Something wrong with the attention processors concerning the fused QKV projections."
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice_fused = image[0, -3:, -3:, -1]
output_fused = self.run_pipe(pipe)
pipe.transformer.unfuse_qkv_projections()
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice_disabled = image[0, -3:, -3:, -1]
output_disabled = self.run_pipe(pipe)
assert np.allclose(original_image_slice, image_slice_fused, atol=1e-3, rtol=1e-3), (
"Fusion of QKV projections shouldn't affect the outputs."
assert_tensors_close(
output_fused, original_output, atol=1e-3, rtol=1e-3, msg="Fusion of QKV projections changed the outputs."
)
assert np.allclose(image_slice_fused, image_slice_disabled, atol=1e-3, rtol=1e-3), (
"Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled."
assert_tensors_close(
output_disabled,
output_fused,
atol=1e-3,
rtol=1e-3,
msg="Outputs changed after the fused QKV projections were disabled.",
)
assert np.allclose(original_image_slice, image_slice_disabled, atol=1e-2, rtol=1e-2), (
"Original outputs should match when fused QKV projections are disabled."
assert_tensors_close(
output_disabled,
original_output,
atol=1e-2,
rtol=1e-2,
msg="Original outputs should match when fused QKV projections are disabled.",
)
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusion3Pipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
components = self.get_dummy_components()
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
all_self_attn_layers = [k for k in pipe.transformer.attn_processors.keys() if "attn" in k]
original_attn_procs = pipe.transformer.attn_processors
@@ -270,3 +221,7 @@ class StableDiffusion3PAGPipelineFastTests(unittest.TestCase, PipelineTesterMixi
pag_layers = ["blocks.0", r"blocks\.1"]
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert len(pipe.pag_attn_processors) == 2
class TestStableDiffusion3PAGPipelineMemory(StableDiffusion3PAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SD3 PAG pipeline."""

View File

@@ -1,9 +1,8 @@
import gc
import inspect
import random
import unittest
import numpy as np
import pytest
import torch
from transformers import (
AutoConfig,
@@ -33,29 +32,30 @@ from ...testing_utils import (
torch_device,
)
from ..pipeline_params import (
IMAGE_TO_IMAGE_IMAGE_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
)
from ..test_pipelines_common import (
PipelineTesterMixin,
)
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusion3PAGImg2ImgPipelineFastTests(unittest.TestCase, PipelineTesterMixin):
class StableDiffusion3PAGImg2ImgPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusion3PAGImg2ImgPipeline
params = TEXT_GUIDED_IMAGE_VARIATION_PARAMS.union({"pag_scale", "pag_adaptive_scale"}) - {"height", "width"}
required_optional_params = PipelineTesterMixin.required_optional_params - {"latents"}
batch_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
image_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
image_latens_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_VARIATION_PARAMS.union(
{"pag_scale", "pag_adaptive_scale"}
) - {"height", "width"}
batch_input_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS
test_xformers_attention = False
# Img2img derives the latents from the input image, so `__call__` takes no `latents`.
optional_input_params = frozenset(
["num_inference_steps", "num_images_per_prompt", "generator", "output_type", "return_dict"]
)
# The output resolution follows the 32x32 input image.
output_shape = (3, 32, 32)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -128,93 +128,49 @@ class StableDiffusion3PAGImg2ImgPipelineFastTests(unittest.TestCase, PipelineTes
"vae": vae,
}
def get_dummy_inputs(self, device, seed=0):
image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device)
def get_dummy_inputs(self):
image = floats_tensor((1, 3, 32, 32), rng=random.Random(0)).to(torch_device)
image = image / 2 + 0.5
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device="cpu").manual_seed(seed)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"image": image,
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"pag_scale": 0.7,
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusion3Img2ImgPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
class TestStableDiffusion3PAGImg2ImgPipeline(StableDiffusion3PAGImg2ImgPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusion3Img2ImgPipeline
# SD3's denoiser is a transformer, so PAG resolves per transformer block rather than the UNet's mid/up/down.
pag_inference_applied_layers = ["blocks.0"]
# Only the "PAG off reproduces the base pipeline" leg was asserted before the migration.
check_pag_changes_output = False
# fmt: off
expected_pag_slice = torch.tensor([0.741577, 0.5491905, 0.59911674, 0.7702221, 0.65531653, 0.60989463, 0.491042, 0.5380116, 0.5592475])
# fmt: on
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
components = self.get_dummy_components()
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["blocks.0"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
32,
32,
3,
), f"the shape of the output image should be (1, 32, 32, 3) but got {image.shape}"
expected_slice = np.array(
[0.741577, 0.5491905, 0.59911674, 0.7702221, 0.65531653, 0.60989463, 0.491042, 0.5380116, 0.5592475]
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
class TestStableDiffusion3PAGImg2ImgPipelineMemory(StableDiffusion3PAGImg2ImgPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SD3 PAG img2img pipeline."""
@slow
@require_torch_accelerator
class StableDiffusion3PAGImg2ImgPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusion3PAGImg2ImgPipelineIntegration:
pipeline_class = StableDiffusion3PAGImg2ImgPipeline
repo_id = "stabilityai/stable-diffusion-3-medium-diffusers"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -14,17 +14,15 @@
# limitations under the License.
import gc
import inspect
import random
import unittest
import numpy as np
import pytest
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
from diffusers import (
AutoencoderKL,
AutoencoderTiny,
AutoPipelineForImage2Image,
EulerDiscreteScheduler,
StableDiffusionImg2ImgPipeline,
@@ -42,36 +40,34 @@ from ...testing_utils import (
torch_device,
)
from ..pipeline_params import (
IMAGE_TO_IMAGE_IMAGE_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
IPAdapterTesterMixin,
PipelineKarrasSchedulerTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionPAGImg2ImgPipelineFastTests(
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineKarrasSchedulerTesterMixin,
PipelineTesterMixin,
unittest.TestCase,
):
class StableDiffusionPAGImg2ImgPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionPAGImg2ImgPipeline
params = TEXT_GUIDED_IMAGE_VARIATION_PARAMS.union({"pag_scale", "pag_adaptive_scale"}) - {"height", "width"}
required_optional_params = PipelineTesterMixin.required_optional_params - {"latents"}
batch_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
image_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
image_latents_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_VARIATION_PARAMS.union(
{"pag_scale", "pag_adaptive_scale"}
) - {"height", "width"}
batch_input_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS
# Img2img derives the latents from the input image, so `__call__` takes no `latents`.
optional_input_params = frozenset(
["num_inference_steps", "num_images_per_prompt", "generator", "output_type", "return_dict"]
)
# The output resolution follows the 32x32 input image.
output_shape = (3, 32, 32)
def get_dummy_components(self, time_cond_proj_dim=None):
torch.manual_seed(0)
@@ -117,7 +113,7 @@ class StableDiffusionPAGImg2ImgPipelineFastTests(
text_encoder = CLIPTextModel(text_encoder_config)
tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
components = {
return {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
@@ -127,111 +123,58 @@ class StableDiffusionPAGImg2ImgPipelineFastTests(
"feature_extractor": None,
"image_encoder": None,
}
return components
def get_dummy_tiny_autoencoder(self):
return AutoencoderTiny(in_channels=3, out_channels=3, latent_channels=4)
def get_dummy_inputs(self, device, seed=0):
image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device)
def get_dummy_inputs(self):
image = floats_tensor((1, 3, 32, 32), rng=random.Random(0)).to(torch_device)
image = image / 2 + 0.5
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"image": image,
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 6.0,
"pag_scale": 0.9,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusionImg2ImgPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
32,
32,
3,
), f"the shape of the output image should be (1, 32, 32, 3) but got {image.shape}"
expected_slice = np.array(
[0.4508819, 0.49191576, 0.42664427, 0.66448534, 0.5606137, 0.43760118, 0.58251137, 0.5944448, 0.51642907]
)
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
class TestStableDiffusionPAGImg2ImgPipeline(StableDiffusionPAGImg2ImgPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusionImg2ImgPipeline
# fmt: off
expected_pag_slice = torch.tensor([0.4508819, 0.49191576, 0.42664427, 0.66448534, 0.5606137, 0.43760118, 0.58251137, 0.5944448, 0.51642907])
# fmt: on
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"do_classifier_free_guidance": self.get_dummy_inputs(device=torch_device).get("guidance_scale", 1.0) > 1.0,
"do_classifier_free_guidance": self.get_dummy_inputs().get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict)
class TestStableDiffusionPAGImg2ImgPipelineMemory(StableDiffusionPAGImg2ImgPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SD PAG img2img pipeline."""
class TestStableDiffusionPAGImg2ImgPipelineIPAdapter(
StableDiffusionPAGImg2ImgPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SD PAG img2img pipeline."""
@slow
@require_torch_accelerator
class StableDiffusionPAGImg2ImgPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusionPAGImg2ImgPipelineIntegration:
pipeline_class = StableDiffusionPAGImg2ImgPipeline
repo_id = "Jiali/stable-diffusion-1.5"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -15,9 +15,9 @@
import gc
import random
import unittest
import numpy as np
import pytest
import torch
from PIL import Image
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
@@ -26,6 +26,7 @@ from diffusers import (
AutoencoderKL,
AutoPipelineForInpainting,
PNDMScheduler,
StableDiffusionInpaintPipeline,
StableDiffusionPAGInpaintPipeline,
UNet2DConditionModel,
)
@@ -44,32 +45,29 @@ from ..pipeline_params import (
TEXT_GUIDED_IMAGE_INPAINTING_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionPAGInpaintPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionPAGInpaintPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionPAGInpaintPipeline
params = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS
image_params = frozenset([])
image_latents_params = frozenset([])
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS.union(
{"pag_scale", "pag_adaptive_scale"}
)
batch_input_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union(
{"add_text_embeds", "add_time_ids", "mask", "masked_image_latents"}
)
# The output resolution follows the 64x64 input image.
output_shape = (3, 64, 64)
def get_dummy_components(self, time_cond_proj_dim=None):
torch.manual_seed(0)
@@ -109,7 +107,7 @@ class StableDiffusionPAGInpaintPipelineFastTests(
text_encoder = CLIPTextModel(text_encoder_config)
tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
components = {
return {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
@@ -119,42 +117,39 @@ class StableDiffusionPAGInpaintPipelineFastTests(
"feature_extractor": None,
"image_encoder": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
def get_dummy_inputs(self):
# TODO: use tensor inputs instead of PIL, this is here just to leave the old expected_slices untouched
image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device)
image = floats_tensor((1, 3, 32, 32), rng=random.Random(0)).to(torch_device)
image = image.cpu().permute(0, 2, 3, 1)[0]
init_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64))
# create mask
image[8:, 8:, :] = 255
mask_image = Image.fromarray(np.uint8(image)).convert("L").resize((64, 64))
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"image": init_image,
"mask_image": mask_image,
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 6.0,
"strength": 1.0,
"pag_scale": 0.9,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
class TestStableDiffusionPAGInpaintPipeline(StableDiffusionPAGInpaintPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusionInpaintPipeline
# fmt: off
expected_pag_slice = torch.tensor([0.7173, 0.5821, 0.6031, 0.5765, 0.6412, 0.6558, 0.5803, 0.5675, 0.5246])
# fmt: on
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
# pag_applied_layers = ["mid","up","down"] should apply to all self-attention layers
all_self_attn_layers = [k for k in pipe.unet.attn_processors.keys() if "attn1" in k]
@@ -192,7 +187,7 @@ class StableDiffusionPAGInpaintPipelineFastTests(
# pag_applied_layers = ["mid.block_0.attentions_1"] does not exist in the model
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["mid_block.attentions.1"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
# pag_applied_layers = "down" should apply to all self-attention layers in down_blocks
@@ -207,7 +202,7 @@ class StableDiffusionPAGInpaintPipelineFastTests(
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["down_blocks.0"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
pipe.unet.set_attn_processor(original_attn_procs.copy())
@@ -220,50 +215,41 @@ class StableDiffusionPAGInpaintPipelineFastTests(
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert len(pipe.pag_attn_processors) == 1
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array([0.7173, 0.5821, 0.6031, 0.5765, 0.6412, 0.6558, 0.5803, 0.5675, 0.5246])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"do_classifier_free_guidance": self.get_dummy_inputs(device=torch_device).get("guidance_scale", 1.0) > 1.0,
"do_classifier_free_guidance": self.get_dummy_inputs().get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict, atol=1e-3, rtol=1e-3)
class TestStableDiffusionPAGInpaintPipelineMemory(StableDiffusionPAGInpaintPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SD PAG inpaint pipeline."""
class TestStableDiffusionPAGInpaintPipelineIPAdapter(
StableDiffusionPAGInpaintPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SD PAG inpaint pipeline."""
class TestStableDiffusionPAGInpaintPipelineFromPipe(
StableDiffusionPAGInpaintPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` round-trip tests against `StableDiffusionPipeline`."""
@slow
@require_torch_accelerator
class StableDiffusionPAGPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusionPAGInpaintPipelineIntegration:
pipeline_class = StableDiffusionPAGInpaintPipeline
repo_id = "stable-diffusion-v1-5/stable-diffusion-v1-5"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -14,10 +14,9 @@
# limitations under the License.
import gc
import inspect
import unittest
import numpy as np
import pytest
import torch
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTextModelWithProjection, CLIPTokenizer
@@ -41,33 +40,26 @@ from ...testing_utils import (
from ..pipeline_params import (
TEXT_TO_IMAGE_BATCH_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
TEXT_TO_IMAGE_IMAGE_PARAMS,
TEXT_TO_IMAGE_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionXLPAGPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionXLPAGPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionXLPAGPipeline
params = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
output_shape = (3, 64, 64)
def get_dummy_components(self, time_cond_proj_dim=None):
# Copied from tests.pipelines.stable_diffusion_xl.test_stable_diffusion_xl.StableDiffusionXLPipelineTesterConfig.get_dummy_components
@@ -129,7 +121,7 @@ class StableDiffusionXLPAGPipelineFastTests(
text_encoder_2 = CLIPTextModelWithProjection(text_encoder_config)
tokenizer_2 = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip")
components = {
return {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
@@ -140,67 +132,28 @@ class StableDiffusionXLPAGPipelineFastTests(
"image_encoder": None,
"feature_extractor": None,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"pag_scale": 0.9,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd = StableDiffusionXLPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
class TestStableDiffusionXLPAGPipeline(StableDiffusionXLPAGPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusionXLPipeline
# fmt: off
expected_pag_slice = torch.tensor([0.5565, 0.5305, 0.4652, 0.4330, 0.4823, 0.4640, 0.5191, 0.4983, 0.4684])
# fmt: on
def test_pag_applied_layers(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
# base pipeline
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline()
# pag_applied_layers = ["mid","up","down"] should apply to all self-attention layers
all_self_attn_layers = [k for k in pipe.unet.attn_processors.keys() if "attn1" in k]
@@ -234,7 +187,7 @@ class StableDiffusionXLPAGPipelineFastTests(
# pag_applied_layers = ["mid.block_0.attentions_1"] does not exist in the model
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["mid_block.attentions.1"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
# pag_applied_layers = "down" should apply to all self-attention layers in down_blocks
@@ -249,7 +202,7 @@ class StableDiffusionXLPAGPipelineFastTests(
pipe.unet.set_attn_processor(original_attn_procs.copy())
pag_layers = ["down_blocks.0"]
with self.assertRaises(ValueError):
with pytest.raises(ValueError):
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
pipe.unet.set_attn_processor(original_attn_procs.copy())
@@ -262,47 +215,34 @@ class StableDiffusionXLPAGPipelineFastTests(
pipe._set_pag_attn_processor(pag_applied_layers=pag_layers, do_classifier_free_guidance=False)
assert len(pipe.pag_attn_processors) == 2
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array([0.5565, 0.5305, 0.4652, 0.4330, 0.4823, 0.4640, 0.5191, 0.4983, 0.4684])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
@unittest.skip("We test this functionality elsewhere already.")
@pytest.mark.skip("We test this functionality elsewhere already.")
def test_save_load_optional_components(self):
pass
class TestStableDiffusionXLPAGPipelineMemory(StableDiffusionXLPAGPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SDXL PAG pipeline."""
class TestStableDiffusionXLPAGPipelineIPAdapter(StableDiffusionXLPAGPipelineTesterConfig, IPAdapterTesterMixin):
"""IP-Adapter tests for the SDXL PAG pipeline."""
class TestStableDiffusionXLPAGPipelineFromPipe(StableDiffusionXLPAGPipelineTesterConfig, FromPipeTesterMixin):
"""`from_pipe` round-trip tests against `StableDiffusionXLPipeline`."""
@slow
@require_torch_accelerator
class StableDiffusionXLPAGPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusionXLPAGPipelineIntegration:
pipeline_class = StableDiffusionXLPAGPipeline
repo_id = "stabilityai/stable-diffusion-xl-base-1.0"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -14,11 +14,10 @@
# limitations under the License.
import gc
import inspect
import random
import unittest
import numpy as np
import pytest
import torch
from transformers import (
CLIPImageProcessor,
@@ -49,37 +48,33 @@ from ...testing_utils import (
torch_device,
)
from ..pipeline_params import (
IMAGE_TO_IMAGE_IMAGE_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS,
TEXT_GUIDED_IMAGE_VARIATION_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionXLPAGImg2ImgPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionXLPAGImg2ImgPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionXLPAGImg2ImgPipeline
params = TEXT_GUIDED_IMAGE_VARIATION_PARAMS.union({"pag_scale", "pag_adaptive_scale"}) - {"height", "width"}
batch_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
image_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
image_latents_params = IMAGE_TO_IMAGE_IMAGE_PARAMS
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_VARIATION_PARAMS.union(
{"pag_scale", "pag_adaptive_scale"}
) - {"height", "width"}
batch_input_params = TEXT_GUIDED_IMAGE_VARIATION_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union(
{"add_text_embeds", "add_time_ids", "add_neg_time_ids"}
)
# The output resolution follows the 32x32 input image.
output_shape = (3, 32, 32)
# based on tests.pipelines.stable_diffusion_xl.test_stable_diffusion_xl_img2img_pipeline.get_dummy_components
def get_dummy_components(
@@ -183,101 +178,64 @@ class StableDiffusionXLPAGImg2ImgPipelineFastTests(
# based on tests.pipelines.stable_diffusion_xl.test_stable_diffusion_xl_img2img.StableDiffusionXLImg2ImgPipelineTesterConfig
# add `pag_scale` to the inputs
def get_dummy_inputs(self, device, seed=0):
image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device)
def get_dummy_inputs(self):
image = floats_tensor((1, 3, 32, 32), rng=random.Random(0)).to(torch_device)
image = image / 2 + 0.5
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"image": image,
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"pag_scale": 3.0,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"strength": 0.8,
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components(requires_aesthetics_score=True)
# base pipeline
pipe_sd = StableDiffusionXLImg2ImgPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
class TestStableDiffusionXLPAGImg2ImgPipeline(StableDiffusionXLPAGImg2ImgPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusionXLImg2ImgPipeline
# The expected slice below was recorded against the aesthetics-score configuration.
pag_component_kwargs = {"requires_aesthetics_score": True}
# fmt: off
expected_pag_slice = torch.tensor([0.4566, 0.4907, 0.4374, 0.6633, 0.5626, 0.4494, 0.5771, 0.6011, 0.5245])
# fmt: on
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components(requires_aesthetics_score=True)
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
32,
32,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array([0.4566, 0.4907, 0.4374, 0.6633, 0.5626, 0.4494, 0.5771, 0.6011, 0.5245])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
@unittest.skip("We test this functionality elsewhere already.")
@pytest.mark.skip("We test this functionality elsewhere already.")
def test_save_load_optional_components(self):
pass
class TestStableDiffusionXLPAGImg2ImgPipelineMemory(
StableDiffusionXLPAGImg2ImgPipelineTesterConfig, MemoryTesterMixin
):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SDXL PAG img2img pipeline."""
class TestStableDiffusionXLPAGImg2ImgPipelineIPAdapter(
StableDiffusionXLPAGImg2ImgPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SDXL PAG img2img pipeline."""
class TestStableDiffusionXLPAGImg2ImgPipelineFromPipe(
StableDiffusionXLPAGImg2ImgPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` round-trip tests against `StableDiffusionXLPipeline`."""
@slow
@require_torch_accelerator
class StableDiffusionXLPAGImg2ImgPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusionXLPAGImg2ImgPipelineIntegration:
repo_id = "stabilityai/stable-diffusion-xl-base-1.0"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -14,11 +14,10 @@
# limitations under the License.
import gc
import inspect
import random
import unittest
import numpy as np
import pytest
import torch
from PIL import Image
from transformers import (
@@ -54,32 +53,29 @@ from ..pipeline_params import (
TEXT_GUIDED_IMAGE_INPAINTING_PARAMS,
TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS,
)
from ..test_pipelines_common import (
from ..testing_utils import (
BasePipelineTesterConfig,
FromPipeTesterMixin,
IPAdapterTesterMixin,
PipelineFromPipeTesterMixin,
PipelineLatentTesterMixin,
PipelineTesterMixin,
MemoryTesterMixin,
)
from .testing_utils import PAGPipelineTesterMixin
enable_full_determinism()
class StableDiffusionXLPAGInpaintPipelineFastTests(
PipelineTesterMixin,
IPAdapterTesterMixin,
PipelineLatentTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
class StableDiffusionXLPAGInpaintPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = StableDiffusionXLPAGInpaintPipeline
params = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS
image_params = frozenset([])
image_latents_params = frozenset([])
required_input_params_in_call_signature = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS.union(
{"pag_scale", "pag_adaptive_scale"}
)
batch_input_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS
callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union(
{"add_text_embeds", "add_time_ids", "mask", "masked_image_latents"}
)
# The output resolution follows the 64x64 input image.
output_shape = (3, 64, 64)
# based on tests.pipelines.stable_diffusion_xl.test_stable_diffusion_xl_inpaint.StableDiffusionXLInpaintPipelineTesterConfig.get_dummy_components
def get_dummy_components(
@@ -181,108 +177,71 @@ class StableDiffusionXLPAGInpaintPipelineFastTests(
}
return components
def get_dummy_inputs(self, device, seed=0):
def get_dummy_inputs(self):
# TODO: use tensor inputs instead of PIL, this is here just to leave the old expected_slices untouched
image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device)
image = floats_tensor((1, 3, 32, 32), rng=random.Random(0)).to(torch_device)
image = image.cpu().permute(0, 2, 3, 1)[0]
init_image = Image.fromarray(np.uint8(image)).convert("RGB").resize((64, 64))
# create mask
image[8:, 8:, :] = 255
mask_image = Image.fromarray(np.uint8(image)).convert("L").resize((64, 64))
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
return {
"prompt": "A painting of a squirrel eating a burger",
"image": init_image,
"mask_image": mask_image,
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 6.0,
"strength": 1.0,
"pag_scale": 0.9,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
def test_pag_disable_enable(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components(requires_aesthetics_score=True)
# base pipeline
pipe_sd = StableDiffusionXLInpaintPipeline(**components)
pipe_sd = pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
class TestStableDiffusionXLPAGInpaintPipeline(StableDiffusionXLPAGInpaintPipelineTesterConfig, PAGPipelineTesterMixin):
base_pipeline_class = StableDiffusionXLInpaintPipeline
# The expected slice below was recorded against the aesthetics-score configuration.
pag_component_kwargs = {"requires_aesthetics_score": True}
# fmt: off
expected_pag_slice = torch.tensor([0.7893, 0.5446, 0.5826, 0.6441, 0.6660, 0.7566, 0.6605, 0.5838, 0.5160])
# fmt: on
inputs = self.get_dummy_inputs(device)
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out = pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag = self.pipeline_class(**components)
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs["pag_scale"] = 0.0
out_pag_disabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
out_pag_enabled = pipe_pag(**inputs).images[0, -3:, -3:, -1]
assert np.abs(out.flatten() - out_pag_disabled.flatten()).max() < 1e-3
assert np.abs(out.flatten() - out_pag_enabled.flatten()).max() > 1e-3
def test_pag_inference(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components(requires_aesthetics_score=True)
pipe_pag = self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag = pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
image = pipe_pag(**inputs).images
image_slice = image[0, -3:, -3:, -1]
assert image.shape == (
1,
64,
64,
3,
), f"the shape of the output image should be (1, 64, 64, 3) but got {image.shape}"
expected_slice = np.array([0.7893, 0.5446, 0.5826, 0.6441, 0.6660, 0.7566, 0.6605, 0.5838, 0.5160])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
assert max_diff < 1e-3, f"output is different from expected, {image_slice.flatten()}"
@unittest.skip("We test this functionality elsewhere already.")
@pytest.mark.skip("We test this functionality elsewhere already.")
def test_save_load_optional_components(self):
pass
class TestStableDiffusionXLPAGInpaintPipelineMemory(
StableDiffusionXLPAGInpaintPipelineTesterConfig, MemoryTesterMixin
):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the SDXL PAG inpaint pipeline."""
class TestStableDiffusionXLPAGInpaintPipelineIPAdapter(
StableDiffusionXLPAGInpaintPipelineTesterConfig, IPAdapterTesterMixin
):
"""IP-Adapter tests for the SDXL PAG inpaint pipeline."""
class TestStableDiffusionXLPAGInpaintPipelineFromPipe(
StableDiffusionXLPAGInpaintPipelineTesterConfig, FromPipeTesterMixin
):
"""`from_pipe` round-trip tests against `StableDiffusionXLPipeline`."""
@slow
@require_torch_accelerator
class StableDiffusionXLPAGInpaintPipelineIntegrationTests(unittest.TestCase):
class TestStableDiffusionXLPAGInpaintPipelineIntegration:
repo_id = "stabilityai/stable-diffusion-xl-base-1.0"
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)

View File

@@ -0,0 +1,121 @@
# coding=utf-8
# Copyright 2026 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 inspect
import pytest
from ...testing_utils import assert_tensors_close, torch_device
from ..testing_utils import PipelineTesterMixin
class PAGPipelineTesterMixin(PipelineTesterMixin):
"""`PipelineTesterMixin` plus the two tests every PAG pipeline shares.
A PAG pipeline is an existing pipeline with perturbed-attention guidance layered on, so the tests all take the
same shape: run the pipeline it derives from, then check that PAG at `pag_scale=0.0` reproduces that output and
that enabling PAG moves it. Subclasses supply the knobs below; anything pipeline-specific (which layers PAG
resolves to, for instance) stays a method on the concrete test class.
"""
# The non-PAG pipeline this one derives from. Required.
base_pipeline_class = None
# `pag_applied_layers` for the "PAG enabled" leg of `test_pag_disable_enable`. `None` keeps the pipeline default.
pag_enabled_applied_layers = ["mid", "up", "down"]
# `pag_scale` for the "PAG enabled" leg. `None` keeps the value from `get_dummy_inputs()`.
pag_enabled_scale = None
# Some PAG pipelines only assert the disabled leg: their dummy denoiser is small enough that PAG's effect is not
# reliably above the tolerance. Set to `False` there.
check_pag_changes_output = True
# `pag_applied_layers` the `test_pag_inference` pipeline is built with. `None` keeps the pipeline default.
pag_inference_applied_layers = ["mid", "up", "down"]
# CPU-specific expected slice for `test_pag_inference`, laid out as the flattened `output[0, -1, -3:, -3:]`
# corner of the `"pt"` output. `None` skips the test.
expected_pag_slice = None
# Extra kwargs for the `get_dummy_components()` call the two tests below build their pipelines from, when the
# PAG comparison needs a configuration other than the default one — the SDXL img2img and inpaint testers pin
# `requires_aesthetics_score=True`, which is what their expected slices were recorded against.
pag_component_kwargs = {}
def get_pag_components(self):
return self.get_dummy_components(**self.pag_component_kwargs)
def get_pag_pipeline(self, components=None, **pag_kwargs):
"""Build the pipeline under test with explicit PAG constructor kwargs (`pag_applied_layers`, ...)."""
components = components if components is not None else self.get_dummy_components()
pipe = self.pipeline_class(**components, **pag_kwargs)
pipe.set_progress_bar_config(disable=None)
return pipe
def test_pag_disable_enable(self):
# Run on CPU to keep the device-dependent `torch.Generator` deterministic.
components = self.get_pag_components()
# base pipeline (expect same output when pag is disabled)
pipe_base = self.base_pipeline_class(**components)
pipe_base.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs()
del inputs["pag_scale"]
assert "pag_scale" not in inspect.signature(pipe_base.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_base.__class__.__name__}."
)
out = pipe_base(**inputs)[0]
# pag disabled with pag_scale=0.0
pipe_pag = self.get_pipeline(**self.get_pag_components())
out_pag_disabled = self.run_pipe(pipe_pag, pag_scale=0.0)
assert_tensors_close(out_pag_disabled, out, atol=1e-3, msg="PAG at `pag_scale=0.0` changed the output.")
if not self.check_pag_changes_output:
return
# pag enabled
pag_kwargs = {}
if self.pag_enabled_applied_layers is not None:
pag_kwargs["pag_applied_layers"] = self.pag_enabled_applied_layers
pipe_pag = self.get_pag_pipeline(self.get_pag_components(), **pag_kwargs)
extra_inputs = {} if self.pag_enabled_scale is None else {"pag_scale": self.pag_enabled_scale}
out_pag_enabled = self.run_pipe(pipe_pag, **extra_inputs)
assert (out - out_pag_enabled).abs().max() > 1e-3, "Enabling PAG should change the output."
def test_pag_inference(self):
if self.expected_pag_slice is None:
pytest.skip(f"No CPU expected slice pinned for {self.pipeline_class.__name__}.")
if torch_device != "cpu":
pytest.skip("The expected slice is CPU-specific.")
pag_kwargs = (
{}
if self.pag_inference_applied_layers is None
else {"pag_applied_layers": self.pag_inference_applied_layers}
)
pipe_pag = self.get_pag_pipeline(self.get_pag_components(), **pag_kwargs)
image = pipe_pag(**self.get_dummy_inputs())[0]
assert image.shape == (1, *self.output_shape), (
f"the shape of the output image should be {(1, *self.output_shape)} but got {tuple(image.shape)}"
)
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), self.expected_pag_slice, atol=1e-3)

View File

@@ -14,10 +14,9 @@
# limitations under the License.
import gc
import tempfile
import unittest
import numpy as np
import pytest
import torch
from transformers import AutoConfig, AutoTokenizer, T5EncoderModel
@@ -30,6 +29,7 @@ from diffusers import (
from diffusers.utils.import_utils import is_torch_neuronx_available
from ...testing_utils import (
assert_tensors_close,
backend_empty_cache,
backend_synchronize,
enable_full_determinism,
@@ -39,23 +39,19 @@ from ...testing_utils import (
slow,
torch_device,
)
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_IMAGE_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineTesterMixin, to_np
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin
enable_full_determinism()
class PixArtAlphaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
class PixArtAlphaPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = PixArtAlphaPipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params = PipelineTesterMixin.required_optional_params
test_layerwise_casting = True
test_group_offloading = True
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
# `transformer.sample_size` (8) * `vae_scale_factor` (8) / 8 -> the dummy transformer generates 8x8 images.
output_shape = (3, 8, 8)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -87,98 +83,74 @@ class PixArtAlphaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5")
components = {
return {
"transformer": transformer.eval(),
"vae": vae.eval(),
"scheduler": scheduler,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"use_resolution_binning": False,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
@unittest.skip("Not supported.")
def test_sequential_cpu_offload_forward_pass(self):
# TODO(PVP, Sayak) need to fix later
return
class TestPixArtAlphaPipeline(PixArtAlphaPipelineTesterConfig, PipelineTesterMixin):
def test_inference(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs()).images
assert image.shape == (1, *self.output_shape)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice = image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (1, 8, 8, 3))
expected_slice = np.array([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.483, 0.2583, 0.5331, 0.4852])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
# fmt: off
expected_slice = torch.tensor([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.483, 0.2583, 0.5331, 0.4852])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_inference_non_square_images(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs(), height=32, width=48).images
assert image.shape == (1, 3, 32, 48)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs, height=32, width=48).images
image_slice = image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (1, 32, 48, 3))
# fmt: off
expected_slice = torch.tensor([0.6493, 0.537, 0.4081, 0.4762, 0.3695, 0.4711, 0.3026, 0.5218, 0.5263])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
expected_slice = np.array([0.6493, 0.537, 0.4081, 0.4762, 0.3695, 0.4711, 0.3026, 0.5218, 0.5263])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
@unittest.skip("Test is already covered through encode_prompt isolation.")
@pytest.mark.skip("Test is already covered through encode_prompt isolation.")
def test_save_load_optional_components(self):
pass
def test_inference_with_embeddings_and_multiple_images(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
def test_inference_with_embeddings_and_multiple_images(self, tmp_path):
pipe = self.get_pipeline().to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
prompt = inputs["prompt"]
generator = inputs["generator"]
num_inference_steps = inputs["num_inference_steps"]
output_type = inputs["output_type"]
prompt_embeds, prompt_attn_mask, negative_prompt_embeds, neg_prompt_attn_mask = pipe.encode_prompt(prompt)
inputs = self.get_dummy_inputs()
prompt_embeds, prompt_attn_mask, negative_prompt_embeds, neg_prompt_attn_mask = pipe.encode_prompt(
inputs["prompt"]
)
# inputs with prompt converted to embeddings
inputs = {
embedding_inputs = {
"prompt_embeds": prompt_embeds,
"prompt_attention_mask": prompt_attn_mask,
"negative_prompt": None,
"negative_prompt_embeds": negative_prompt_embeds,
"negative_prompt_attention_mask": neg_prompt_attn_mask,
"generator": generator,
"num_inference_steps": num_inference_steps,
"output_type": output_type,
"generator": inputs["generator"],
"num_inference_steps": inputs["num_inference_steps"],
"output_type": inputs["output_type"],
"num_images_per_prompt": 2,
"use_resolution_binning": False,
}
@@ -187,97 +159,76 @@ class PixArtAlphaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
for optional_component in pipe._optional_components:
setattr(pipe, optional_component, None)
output = pipe(**inputs)[0]
output = pipe(**embedding_inputs)[0]
with tempfile.TemporaryDirectory() as tmpdir:
pipe.save_pretrained(tmpdir)
pipe_loaded = self.pipeline_class.from_pretrained(tmpdir)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
pipe.save_pretrained(tmp_path)
pipe_loaded = self.pipeline_class.from_pretrained(tmp_path)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
for optional_component in pipe._optional_components:
self.assertTrue(
getattr(pipe_loaded, optional_component) is None,
f"`{optional_component}` did not stay set to None after loading.",
assert getattr(pipe_loaded, optional_component) is None, (
f"`{optional_component}` did not stay set to None after loading."
)
inputs = self.get_dummy_inputs(torch_device)
embedding_inputs["generator"] = self.get_generator(0)
output_loaded = pipe_loaded(**embedding_inputs)[0]
generator = inputs["generator"]
num_inference_steps = inputs["num_inference_steps"]
output_type = inputs["output_type"]
# inputs with prompt converted to embeddings
inputs = {
"prompt_embeds": prompt_embeds,
"prompt_attention_mask": prompt_attn_mask,
"negative_prompt": None,
"negative_prompt_embeds": negative_prompt_embeds,
"negative_prompt_attention_mask": neg_prompt_attn_mask,
"generator": generator,
"num_inference_steps": num_inference_steps,
"output_type": output_type,
"num_images_per_prompt": 2,
"use_resolution_binning": False,
}
output_loaded = pipe_loaded(**inputs)[0]
max_diff = np.abs(to_np(output) - to_np(output_loaded)).max()
self.assertLess(max_diff, 1e-4)
assert_tensors_close(
output_loaded, output, atol=1e-4, msg="Output changed after dropping optional components."
)
def test_inference_with_multiple_images_per_prompt(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs(), num_images_per_prompt=2).images
assert image.shape == (2, *self.output_shape)
inputs = self.get_dummy_inputs(device)
inputs["num_images_per_prompt"] = 2
image = pipe(**inputs).images
image_slice = image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (2, 8, 8, 3))
expected_slice = np.array([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.483, 0.2583, 0.5331, 0.4852])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
# fmt: off
expected_slice = torch.tensor([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.483, 0.2583, 0.5331, 0.4852])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_raises_warning_for_mask_feature(self):
device = "cpu"
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs.update({"mask_feature": True})
with self.assertWarns(FutureWarning) as warning_ctx:
with pytest.warns(FutureWarning, match="mask_feature"):
_ = pipe(**inputs).images
assert "mask_feature" in str(warning_ctx.warning)
def test_inference_batch_single_identical(self):
self._test_inference_batch_single_identical(expected_max_diff=1e-3)
super().test_inference_batch_single_identical(expected_max_diff=1e-3)
class TestPixArtAlphaPipelineMemory(PixArtAlphaPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the PixArt-alpha pipeline."""
@pytest.mark.skip("Not supported.")
def test_sequential_cpu_offload_forward_pass(self):
# TODO(PVP, Sayak) need to fix later
pass
@pytest.mark.skip("Not supported.")
def test_sequential_offload_forward_pass_twice(self):
# TODO(PVP, Sayak) need to fix later
pass
@slow
@require_torch_accelerator
class PixArtAlphaPipelineIntegrationTests(unittest.TestCase):
class TestPixArtAlphaPipelineIntegration:
ckpt_id_1024 = "PixArt-alpha/PixArt-XL-2-1024-MS"
ckpt_id_512 = "PixArt-alpha/PixArt-XL-2-512x512"
prompt = "A small cactus with a happy face in the Sahara desert."
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)
@@ -296,7 +247,7 @@ class PixArtAlphaPipelineIntegrationTests(unittest.TestCase):
max_diff = numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice)
# Neuron uses bfloat16 internally which has lower precision than float16 on CUDA
atol = 1e-2 if is_torch_neuronx_available() else 1e-4
self.assertLessEqual(max_diff, atol)
assert max_diff <= atol
def test_pixart_512(self):
generator = torch.Generator("cpu").manual_seed(0)
@@ -314,7 +265,7 @@ class PixArtAlphaPipelineIntegrationTests(unittest.TestCase):
max_diff = numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice)
# Neuron uses bfloat16 internally which has lower precision than float16 on CUDA
atol = 1e-2 if is_torch_neuronx_available() else 1e-4
self.assertLessEqual(max_diff, atol)
assert max_diff <= atol
def test_pixart_1024_without_resolution_binning(self):
generator = torch.manual_seed(0)
@@ -419,9 +370,6 @@ class PixArtAlphaPipelineIntegrationTests(unittest.TestCase):
output_type="np",
).images
self.assertEqual(image.shape, (1, 512, 512, 3))
self.assertFalse(np.isnan(image).any(), "Output contains NaN values")
self.assertTrue(
(image >= 0.0).all() and (image <= 1.0).all(),
"Output pixel values outside [0, 1]",
)
assert image.shape == (1, 512, 512, 3)
assert not np.isnan(image).any(), "Output contains NaN values"
assert (image >= 0.0).all() and (image <= 1.0).all(), "Output pixel values outside [0, 1]"

View File

@@ -14,10 +14,9 @@
# limitations under the License.
import gc
import tempfile
import unittest
import numpy as np
import pytest
import torch
from transformers import AutoConfig, AutoTokenizer, T5EncoderModel
@@ -30,6 +29,7 @@ from diffusers import (
from ...testing_utils import (
Expectations,
assert_tensors_close,
backend_empty_cache,
enable_full_determinism,
numpy_cosine_similarity_distance,
@@ -37,28 +37,25 @@ from ...testing_utils import (
slow,
torch_device,
)
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_IMAGE_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import (
from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS, TEXT_TO_IMAGE_PARAMS
from ..testing_utils import (
BasePipelineTesterConfig,
MemoryTesterMixin,
PipelineTesterMixin,
check_qkv_fusion_matches_attn_procs_length,
check_qkv_fusion_processors_exist,
to_np,
)
enable_full_determinism()
class PixArtSigmaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
class PixArtSigmaPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = PixArtSigmaPipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params = PipelineTesterMixin.required_optional_params
test_layerwise_casting = True
test_group_offloading = True
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = TEXT_TO_IMAGE_BATCH_PARAMS
# `transformer.sample_size` (8) * `vae_scale_factor` (8) / 8 -> the dummy transformer generates 8x8 images.
output_shape = (3, 8, 8)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -90,94 +87,70 @@ class PixArtSigmaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5")
components = {
return {
"transformer": transformer.eval(),
"vae": vae.eval(),
"scheduler": scheduler,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
}
return components
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
inputs = {
def get_dummy_inputs(self):
return {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 5.0,
"use_resolution_binning": False,
"output_type": "np",
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
}
return inputs
@unittest.skip("Not supported.")
def test_sequential_cpu_offload_forward_pass(self):
# TODO(PVP, Sayak) need to fix later
return
class TestPixArtSigmaPipeline(PixArtSigmaPipelineTesterConfig, PipelineTesterMixin):
def test_inference(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs()).images
assert image.shape == (1, *self.output_shape)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice = image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (1, 8, 8, 3))
expected_slice = np.array([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.4830, 0.2583, 0.5331, 0.4852])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
# fmt: off
expected_slice = torch.tensor([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.4830, 0.2583, 0.5331, 0.4852])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
def test_inference_non_square_images(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs(), height=32, width=48).images
assert image.shape == (1, 3, 32, 48)
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs, height=32, width=48).images
image_slice = image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (1, 32, 48, 3))
# fmt: off
expected_slice = torch.tensor([0.6493, 0.5370, 0.4081, 0.4762, 0.3695, 0.4711, 0.3026, 0.5218, 0.5263])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
expected_slice = np.array([0.6493, 0.5370, 0.4081, 0.4762, 0.3695, 0.4711, 0.3026, 0.5218, 0.5263])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
def test_inference_with_embeddings_and_multiple_images(self, tmp_path):
pipe = self.get_pipeline().to(torch_device)
def test_inference_with_embeddings_and_multiple_images(self):
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(torch_device)
pipe.set_progress_bar_config(disable=None)
inputs = self.get_dummy_inputs(torch_device)
prompt = inputs["prompt"]
generator = inputs["generator"]
num_inference_steps = inputs["num_inference_steps"]
output_type = inputs["output_type"]
prompt_embeds, prompt_attn_mask, negative_prompt_embeds, neg_prompt_attn_mask = pipe.encode_prompt(prompt)
inputs = self.get_dummy_inputs()
prompt_embeds, prompt_attn_mask, negative_prompt_embeds, neg_prompt_attn_mask = pipe.encode_prompt(
inputs["prompt"]
)
# inputs with prompt converted to embeddings
inputs = {
embedding_inputs = {
"prompt_embeds": prompt_embeds,
"prompt_attention_mask": prompt_attn_mask,
"negative_prompt": None,
"negative_prompt_embeds": negative_prompt_embeds,
"negative_prompt_attention_mask": neg_prompt_attn_mask,
"generator": generator,
"num_inference_steps": num_inference_steps,
"output_type": output_type,
"generator": inputs["generator"],
"num_inference_steps": inputs["num_inference_steps"],
"output_type": inputs["output_type"],
"num_images_per_prompt": 2,
"use_resolution_binning": False,
}
@@ -186,80 +159,49 @@ class PixArtSigmaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
for optional_component in pipe._optional_components:
setattr(pipe, optional_component, None)
output = pipe(**inputs)[0]
output = pipe(**embedding_inputs)[0]
with tempfile.TemporaryDirectory() as tmpdir:
pipe.save_pretrained(tmpdir)
pipe_loaded = self.pipeline_class.from_pretrained(tmpdir)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
pipe.save_pretrained(tmp_path)
pipe_loaded = self.pipeline_class.from_pretrained(tmp_path)
pipe_loaded.to(torch_device)
pipe_loaded.set_progress_bar_config(disable=None)
for optional_component in pipe._optional_components:
self.assertTrue(
getattr(pipe_loaded, optional_component) is None,
f"`{optional_component}` did not stay set to None after loading.",
assert getattr(pipe_loaded, optional_component) is None, (
f"`{optional_component}` did not stay set to None after loading."
)
inputs = self.get_dummy_inputs(torch_device)
embedding_inputs["generator"] = self.get_generator(0)
output_loaded = pipe_loaded(**embedding_inputs)[0]
generator = inputs["generator"]
num_inference_steps = inputs["num_inference_steps"]
output_type = inputs["output_type"]
# inputs with prompt converted to embeddings
inputs = {
"prompt_embeds": prompt_embeds,
"prompt_attention_mask": prompt_attn_mask,
"negative_prompt": None,
"negative_prompt_embeds": negative_prompt_embeds,
"negative_prompt_attention_mask": neg_prompt_attn_mask,
"generator": generator,
"num_inference_steps": num_inference_steps,
"output_type": output_type,
"num_images_per_prompt": 2,
"use_resolution_binning": False,
}
output_loaded = pipe_loaded(**inputs)[0]
max_diff = np.abs(to_np(output) - to_np(output_loaded)).max()
self.assertLess(max_diff, 1e-4)
assert_tensors_close(
output_loaded, output, atol=1e-4, msg="Output changed after dropping optional components."
)
def test_inference_with_multiple_images_per_prompt(self):
device = "cpu"
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
image = pipe(**self.get_dummy_inputs(), num_images_per_prompt=2).images
assert image.shape == (2, *self.output_shape)
inputs = self.get_dummy_inputs(device)
inputs["num_images_per_prompt"] = 2
image = pipe(**inputs).images
image_slice = image[0, -3:, -3:, -1]
# fmt: off
expected_slice = torch.tensor([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.4830, 0.2583, 0.5331, 0.4852])
# fmt: on
assert_tensors_close(image[0, -1, -3:, -3:].flatten(), expected_slice, atol=1e-3)
self.assertEqual(image.shape, (2, 8, 8, 3))
expected_slice = np.array([0.6319, 0.3526, 0.3806, 0.6327, 0.4639, 0.4830, 0.2583, 0.5331, 0.4852])
max_diff = np.abs(image_slice.flatten() - expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
@unittest.skip("Test is already covered through encode_prompt isolation.")
@pytest.mark.skip("Test is already covered through encode_prompt isolation.")
def test_save_load_optional_components(self):
pass
def test_inference_batch_single_identical(self):
self._test_inference_batch_single_identical(expected_max_diff=1e-3)
super().test_inference_batch_single_identical(expected_max_diff=1e-3)
def test_fused_qkv_projections(self):
device = "cpu" # ensure determinism for the device-dependent torch.Generator
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe = pipe.to(device)
pipe.set_progress_bar_config(disable=None)
# Run on CPU to keep the device-dependent `torch.Generator` deterministic.
pipe = self.get_pipeline()
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
original_image_slice = image[0, -3:, -3:, -1]
original_output = self.run_pipe(pipe)
# TODO (sayakpaul): will refactor this once `fuse_qkv_projections()` has been added
# to the pipeline level.
@@ -271,40 +213,56 @@ class PixArtSigmaPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
pipe.transformer, pipe.transformer.original_attn_processors
), "Something wrong with the attention processors concerning the fused QKV projections."
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice_fused = image[0, -3:, -3:, -1]
output_fused = self.run_pipe(pipe)
pipe.transformer.unfuse_qkv_projections()
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs).images
image_slice_disabled = image[0, -3:, -3:, -1]
output_disabled = self.run_pipe(pipe)
assert np.allclose(original_image_slice, image_slice_fused, atol=1e-3, rtol=1e-3), (
"Fusion of QKV projections shouldn't affect the outputs."
assert_tensors_close(
output_fused, original_output, atol=1e-3, rtol=1e-3, msg="Fusion of QKV projections changed the outputs."
)
assert np.allclose(image_slice_fused, image_slice_disabled, atol=1e-3, rtol=1e-3), (
"Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled."
assert_tensors_close(
output_disabled,
output_fused,
atol=1e-3,
rtol=1e-3,
msg="Outputs changed after the fused QKV projections were disabled.",
)
assert np.allclose(original_image_slice, image_slice_disabled, atol=1e-2, rtol=1e-2), (
"Original outputs should match when fused QKV projections are disabled."
assert_tensors_close(
output_disabled,
original_output,
atol=1e-2,
rtol=1e-2,
msg="Original outputs should match when fused QKV projections are disabled.",
)
class TestPixArtSigmaPipelineMemory(PixArtSigmaPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the PixArt-sigma pipeline."""
@pytest.mark.skip("Not supported.")
def test_sequential_cpu_offload_forward_pass(self):
# TODO(PVP, Sayak) need to fix later
pass
@pytest.mark.skip("Not supported.")
def test_sequential_offload_forward_pass_twice(self):
# TODO(PVP, Sayak) need to fix later
pass
@slow
@require_torch_accelerator
class PixArtSigmaPipelineIntegrationTests(unittest.TestCase):
class TestPixArtSigmaPipelineIntegration:
ckpt_id_1024 = "PixArt-alpha/PixArt-Sigma-XL-2-1024-MS"
ckpt_id_512 = "PixArt-alpha/PixArt-Sigma-XL-2-512-MS"
prompt = "A small cactus with a happy face in the Sahara desert."
def setUp(self):
super().setUp()
@pytest.fixture(autouse=True)
def cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
def tearDown(self):
super().tearDown()
yield
gc.collect()
backend_empty_cache(torch_device)
@@ -321,7 +279,7 @@ class PixArtSigmaPipelineIntegrationTests(unittest.TestCase):
expected_slice = np.array([0.4517, 0.4446, 0.4375, 0.449, 0.4399, 0.4365, 0.4583, 0.4629, 0.4473])
max_diff = numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice)
self.assertLessEqual(max_diff, 1e-4)
assert max_diff <= 1e-4
def test_pixart_512(self):
generator = torch.Generator("cpu").manual_seed(0)
@@ -349,7 +307,7 @@ class PixArtSigmaPipelineIntegrationTests(unittest.TestCase):
expected_slice = expected_slices.get_expectation()
max_diff = numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice)
self.assertLessEqual(max_diff, 1e-4)
assert max_diff <= 1e-4
def test_pixart_1024_without_resolution_binning(self):
generator = torch.manual_seed(0)

View File

@@ -13,61 +13,159 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import numpy as np
import pytest
import torch
from diffusers import PNDMPipeline, PNDMScheduler, UNet2DModel
from ...testing_utils import enable_full_determinism, nightly, require_torch, torch_device
from ...testing_utils import (
enable_full_determinism,
nightly,
require_accelerator,
require_torch,
torch_device,
)
from ..pipeline_params import UNCONDITIONAL_IMAGE_GENERATION_BATCH_PARAMS, UNCONDITIONAL_IMAGE_GENERATION_PARAMS
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin
enable_full_determinism()
class PNDMPipelineFastTests(unittest.TestCase):
@property
def dummy_uncond_unet(self):
# `PNDMPipeline.__call__` postprocesses unconditionally — it always runs `image.cpu().permute(0, 2, 3, 1).numpy()`
# and only branches afterwards to decide whether to wrap the result in PIL. There is no `output_type="pt"` path, so
# `get_dummy_inputs` below has to ask for `"np"`, and every shared test that compares outputs with
# `assert_tensors_close` (torch-only) or calls `torch.isnan` on them fails on the numpy array it gets back.
#
# The sibling unconditional pipelines already have a `"pt"` path (`DDIMPipeline`, `DDPMPipeline`); adding one here
# is a `src/` change and out of scope for this test migration, so the affected tests are marked `xfail` rather than
# skipped: whoever adds the `"pt"` branch will see them XPASS and can drop these markers.
NO_PT_OUTPUT = pytest.mark.xfail(
reason="`PNDMPipeline` has no `output_type='pt'` path and always returns a numpy array.",
strict=True,
)
# `PNDMPipeline` samples its initial noise with `randn_tensor(..., device=self.device)`: no `dtype=`, so the noise
# stays float32 and disagrees with a half-precision or layerwise-cast UNet. Same story as above — a `src/` gap, so
# the accelerator-only tests it breaks are marked `xfail`.
UNSUPPORTED_DTYPE = pytest.mark.xfail(
reason="`PNDMPipeline` samples its initial noise without `dtype=self.unet.dtype`, so it stays float32.",
strict=True,
)
# The memory mixin trips over three separate `src/` gaps at once, so its tests carry one marker between them:
# - the numpy output above (`test_group_offloading_inference`, `test_pipeline_level_group_offloading_inference`,
# `test_pipeline_with_accelerator_device_map`),
# - the float32 noise above (`test_layerwise_casting_inference`),
# - `randn_tensor(..., device=self.device)` reading the *pipeline's* device rather than `self._execution_device`,
# which under sequential offload is `meta` (`test_sequential_cpu_offload_forward_pass`,
# `test_sequential_offload_forward_pass_twice`),
# - and no `model_cpu_offload_seq`, which `enable_model_cpu_offload` requires
# (`test_model_cpu_offload_forward_pass`, `test_cpu_offload_forward_pass_twice`).
# `strict=False` because `test_pipeline_level_group_offloading_sanity_checks` never runs the pipeline and so passes
# — it reports XPASS. The class-level marker is what keeps `MemoryTesterMixin`'s own `@is_memory` /
# `@require_accelerator` marks intact; overriding the eight failing tests individually would drop the
# `@require_accelerate_version_greater` gates they are declared with.
UNSUPPORTED_MEMORY_OPTIMIZATIONS = pytest.mark.xfail(
reason=(
"`PNDMPipeline` returns numpy, samples noise without `dtype=`/`_execution_device`, and declares no "
"`model_cpu_offload_seq`."
),
strict=False,
)
class PNDMPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = PNDMPipeline
required_input_params_in_call_signature = UNCONDITIONAL_IMAGE_GENERATION_PARAMS
batch_input_params = UNCONDITIONAL_IMAGE_GENERATION_BATCH_PARAMS
# PNDM is unconditional and samples its own noise: there is no prompt to repeat
# (`num_images_per_prompt`) and no user-suppliable `latents`.
optional_input_params = frozenset(["num_inference_steps", "generator", "output_type", "return_dict"])
# `(height, width, channels)` — the numpy layout, not the `(channels, height, width)` the other configs get
# from `output_type="pt"` (see `NO_PT_OUTPUT` above).
output_shape = (8, 8, 3)
def get_dummy_components(self):
torch.manual_seed(0)
model = UNet2DModel(
block_out_channels=(32, 64),
layers_per_block=2,
sample_size=32,
unet = UNet2DModel(
block_out_channels=(4, 8),
layers_per_block=1,
norm_num_groups=4,
sample_size=8,
in_channels=3,
out_channels=3,
down_block_types=("DownBlock2D", "AttnDownBlock2D"),
up_block_types=("AttnUpBlock2D", "UpBlock2D"),
)
return model
def test_inference(self):
unet = self.dummy_uncond_unet
scheduler = PNDMScheduler()
return {"unet": unet, "scheduler": scheduler}
pndm = PNDMPipeline(unet=unet, scheduler=scheduler)
pndm.to(torch_device)
pndm.set_progress_bar_config(disable=None)
def get_dummy_inputs(self):
return {
"batch_size": 1,
"generator": self.get_generator(0),
# `PNDMScheduler` runs Runge-Kutta warm-up steps, so it needs at least 4 inference steps.
"num_inference_steps": 4,
# `"np"` rather than the usual `"pt"` — see `NO_PT_OUTPUT` above.
"output_type": "np",
}
generator = torch.manual_seed(0)
image = pndm(generator=generator, num_inference_steps=20, output_type="np").images
generator = torch.manual_seed(0)
image_from_tuple = pndm(generator=generator, num_inference_steps=20, output_type="np", return_dict=False)[0]
class TestPNDMPipeline(PNDMPipelineTesterConfig, PipelineTesterMixin):
def test_inference(self):
# Run on CPU: the expected slice below is CPU-specific.
pipe = self.get_pipeline()
image_slice = image[0, -3:, -3:, -1]
image_from_tuple_slice = image_from_tuple[0, -3:, -3:, -1]
image = pipe(**self.get_dummy_inputs()).images
generated_image = image[0]
assert generated_image.shape == self.output_shape
assert image.shape == (1, 32, 32, 3)
expected_slice = np.array([1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0])
expected_slice = np.array([0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0])
assert np.abs(generated_image[-3:, -3:, -1].flatten() - expected_slice).max() < 1e-2
assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2
assert np.abs(image_from_tuple_slice.flatten() - expected_slice).max() < 1e-2
@NO_PT_OUTPUT
def test_save_load_local(self, tmp_path, base_pipe_output, expected_max_difference=5e-4):
super().test_save_load_local(tmp_path, base_pipe_output, expected_max_difference)
@NO_PT_OUTPUT
def test_inference_batch_single_identical(self):
super().test_inference_batch_single_identical()
@NO_PT_OUTPUT
def test_dict_tuple_outputs_equivalent(self):
super().test_dict_tuple_outputs_equivalent()
# The three overrides below re-declare the base methods' skip decorators: overriding a test drops the marks
# the base declared it with, and without them these would run (and xfail for the wrong reason) on CPU.
@NO_PT_OUTPUT
@require_accelerator
def test_to_device(self):
super().test_to_device()
@UNSUPPORTED_DTYPE
@pytest.mark.skipif(torch_device not in ["cuda", "xpu"], reason="half-precision inference requires CUDA or XPU")
@require_accelerator
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16], ids=str)
def test_half_precision_inference_no_nan(self, dtype):
super().test_half_precision_inference_no_nan(dtype)
@UNSUPPORTED_DTYPE
@pytest.mark.skipif(torch_device not in ["cuda", "xpu"], reason="float16 requires CUDA or XPU")
@require_accelerator
def test_save_load_float16(self, tmp_path, expected_max_diff=1e-2):
super().test_save_load_float16(tmp_path, expected_max_diff)
@UNSUPPORTED_MEMORY_OPTIMIZATIONS
class TestPNDMPipelineMemory(PNDMPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the PNDM pipeline."""
@nightly
@require_torch
class PNDMPipelineIntegrationTests(unittest.TestCase):
class TestPNDMPipelineIntegration:
def test_inference_cifar10(self):
model_id = "google/ddpm-cifar10-32"

View File

@@ -1,6 +1,4 @@
import unittest
import numpy as np
import pytest
import torch
from transformers import AutoTokenizer
from transformers.models.t5gemma.configuration_t5gemma import T5GemmaConfig, T5GemmaModuleConfig
@@ -11,26 +9,32 @@ from diffusers.models.transformers.transformer_prx import PRXTransformer2DModel
from diffusers.pipelines.prx.pipeline_prx import PRXPipeline
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
from ...testing_utils import assert_tensors_close
from ..pipeline_params import TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineTesterMixin
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin
class PRXPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
# `T5GemmaEncoder` is instantiated here from a hand-built config rather than loaded from a repo, and transformers v5
# cannot round-trip that through `save_pretrained`/`from_pretrained`, so every test that reloads the pipeline from
# disk is skipped.
T5GEMMA_SERIALIZATION_SKIP_REASON = "Custom T5GemmaEncoder not compatible with transformers v5."
# Both PRX pipelines read `callback_on_step_end`'s inputs out of `locals()` but throw away what the callback
# returns, so a callback that rewrites `latents` (or `prompt_embeds`) has no effect on the denoising loop. Every
# other diffusers pipeline pops those keys back off the returned dict. Fixing it is a `src/` change and out of
# scope for this test migration, so the one shared test that exercises the write-back is marked `xfail`: whoever
# adds the pop-back will see it XPASS and can drop this marker.
CALLBACK_OUTPUTS_IGNORED = pytest.mark.xfail(
reason="`PRX` pipelines discard the dict `callback_on_step_end` returns, so callback edits to `latents` are lost.",
strict=True,
)
class PRXPipelineTesterConfig(BasePipelineTesterConfig):
pipeline_class = PRXPipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = frozenset(["prompt", "negative_prompt", "num_images_per_prompt"])
test_xformers_attention = False
test_layerwise_casting = True
test_group_offloading = True
@classmethod
def setUpClass(cls):
# Ensure PRXPipeline has an _execution_device property expected by __call__
if not isinstance(getattr(PRXPipeline, "_execution_device", None), property):
try:
setattr(PRXPipeline, "_execution_device", property(lambda self: torch.device("cpu")))
except Exception:
pass
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = frozenset(["prompt", "negative_prompt", "num_images_per_prompt"])
output_shape = (3, 32, 32)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -102,116 +106,54 @@ class PRXPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
"tokenizer": tokenizer,
}
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
def get_dummy_inputs(self):
return {
"prompt": "",
"negative_prompt": "",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 1.0,
"height": 32,
"width": 32,
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
"output_type": "pt",
"use_resolution_binning": False,
}
def test_inference(self):
device = "cpu"
components = self.get_dummy_components()
pipe = PRXPipeline(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
try:
pipe.register_to_config(_execution_device="cpu")
except Exception:
pass
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs)[0]
generated_image = image[0]
self.assertEqual(generated_image.shape, (3, 32, 32))
expected_image = torch.zeros(3, 32, 32)
max_diff = np.abs(generated_image - expected_image).max()
self.assertLessEqual(max_diff, 1e10)
class TestPRXPipeline(PRXPipelineTesterConfig, PipelineTesterMixin):
@CALLBACK_OUTPUTS_IGNORED
def test_callback_inputs(self):
components = self.get_dummy_components()
pipe = PRXPipeline(**components)
pipe = pipe.to("cpu")
pipe.set_progress_bar_config(disable=None)
try:
pipe.register_to_config(_execution_device="cpu")
except Exception:
pass
self.assertTrue(
hasattr(pipe, "_callback_tensor_inputs"),
f" {PRXPipeline} should have `_callback_tensor_inputs` that defines a list of tensor variables its callback function can use as inputs",
)
def callback_inputs_subset(pipe, i, t, callback_kwargs):
for tensor_name in callback_kwargs.keys():
assert tensor_name in pipe._callback_tensor_inputs
return callback_kwargs
def callback_inputs_all(pipe, i, t, callback_kwargs):
for tensor_name in pipe._callback_tensor_inputs:
assert tensor_name in callback_kwargs
for tensor_name in callback_kwargs.keys():
assert tensor_name in pipe._callback_tensor_inputs
return callback_kwargs
inputs = self.get_dummy_inputs("cpu")
inputs["callback_on_step_end"] = callback_inputs_subset
inputs["callback_on_step_end_tensor_inputs"] = ["latents"]
_ = pipe(**inputs)[0]
inputs["callback_on_step_end"] = callback_inputs_all
inputs["callback_on_step_end_tensor_inputs"] = pipe._callback_tensor_inputs
_ = pipe(**inputs)[0]
super().test_callback_inputs()
def test_attention_slicing_forward_pass(self, expected_max_diff=1e-3):
if not self.test_attention_slicing:
return
# Run on CPU: sliced attention is compared against a full-attention run of the same pipeline.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
for component in pipe.components.values():
if hasattr(component, "set_default_attn_processor"):
component.set_default_attn_processor()
pipe.to("cpu")
pipe.set_progress_bar_config(disable=None)
def to_np_local(tensor):
if isinstance(tensor, torch.Tensor):
return tensor.detach().cpu().numpy()
return tensor
generator_device = "cpu"
inputs = self.get_dummy_inputs(generator_device)
output_without_slicing = pipe(**inputs)[0]
output_without_slicing = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=1)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing1 = pipe(**inputs)[0]
output_with_slicing_1 = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=2)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing2 = pipe(**inputs)[0]
output_with_slicing_2 = self.run_pipe(pipe)
max_diff1 = np.abs(to_np_local(output_with_slicing1) - to_np_local(output_without_slicing)).max()
max_diff2 = np.abs(to_np_local(output_with_slicing2) - to_np_local(output_without_slicing)).max()
self.assertLess(max(max_diff1, max_diff2), expected_max_diff)
assert_tensors_close(
output_with_slicing_1,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=1) changed the output.",
)
assert_tensors_close(
output_with_slicing_2,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=2) changed the output.",
)
def test_inference_with_autoencoder_dc(self):
"""Test PRXPipeline with AutoencoderDC (DCAE) instead of AutoencoderKL."""
device = "cpu"
"""PRXPipeline should also work with an `AutoencoderDC` (DCAE) in place of the `AutoencoderKL`."""
components = self.get_dummy_components()
torch.manual_seed(0)
@@ -240,43 +182,38 @@ class PRXPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
).eval()
components["vae"] = vae_dc
pipe = self.get_pipeline(**components)
pipe = PRXPipeline(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
assert pipe.vae_scale_factor == vae_dc.spatial_compression_ratio
expected_scale_factor = vae_dc.spatial_compression_ratio
self.assertEqual(pipe.vae_scale_factor, expected_scale_factor)
output = self.run_pipe(pipe)
assert output[0].shape == self.output_shape
assert torch.isfinite(output).all()
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs)[0]
generated_image = image[0]
self.assertEqual(generated_image.shape, (3, 32, 32))
expected_image = torch.zeros(3, 32, 32)
max_diff = np.abs(generated_image - expected_image).max()
self.assertLessEqual(max_diff, 1e10)
@unittest.skip("Custom T5GemmaEncoder not compatible with transformers v5.")
@pytest.mark.skip(T5GEMMA_SERIALIZATION_SKIP_REASON)
def test_loading_with_variants(self):
pass
@unittest.skip("Custom T5GemmaEncoder not compatible with transformers v5.")
def test_pipeline_with_accelerator_device_map(self):
pass
@unittest.skip("Custom T5GemmaEncoder not compatible with transformers v5.")
@pytest.mark.skip(T5GEMMA_SERIALIZATION_SKIP_REASON)
def test_save_load_local(self):
pass
@unittest.skip("Custom T5GemmaEncoder not compatible with transformers v5.")
@pytest.mark.skip(T5GEMMA_SERIALIZATION_SKIP_REASON)
def test_save_load_float16(self):
pass
@pytest.mark.skip(T5GEMMA_SERIALIZATION_SKIP_REASON)
def test_save_load_optional_components(self):
pass
@unittest.skip("Custom T5GemmaEncoder not compatible with transformers v5.")
def test_dtype_dict(self):
@pytest.mark.skip(T5GEMMA_SERIALIZATION_SKIP_REASON)
def test_torch_dtype_dict(self):
pass
@unittest.skip("Custom T5GemmaEncoder not compatible with transformers v5.")
def test_dtype_alias(self):
class TestPRXPipelineMemory(PRXPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the PRX pipeline."""
@pytest.mark.skip(T5GEMMA_SERIALIZATION_SKIP_REASON)
def test_pipeline_with_accelerator_device_map(self):
pass

View File

@@ -1,6 +1,4 @@
import unittest
import numpy as np
import pytest
import torch
from transformers import Qwen2Tokenizer, Qwen3Config, Qwen3Model
@@ -8,19 +6,29 @@ from diffusers.models.transformers.transformer_prx import PRXTransformer2DModel
from diffusers.pipelines.prx.pipeline_prx_pixel import PRXPixelPipeline
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
from ...testing_utils import assert_tensors_close, torch_device
from ..pipeline_params import TEXT_TO_IMAGE_PARAMS
from ..test_pipelines_common import PipelineTesterMixin
from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin
class PRXPixelPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
# Both PRX pipelines read `callback_on_step_end`'s inputs out of `locals()` but throw away what the callback
# returns, so a callback that rewrites `latents` (or `prompt_embeds`) has no effect on the denoising loop. Every
# other diffusers pipeline pops those keys back off the returned dict. Fixing it is a `src/` change and out of
# scope for this test migration, so the one shared test that exercises the write-back is marked `xfail`: whoever
# adds the pop-back will see it XPASS and can drop this marker.
CALLBACK_OUTPUTS_IGNORED = pytest.mark.xfail(
reason="`PRX` pipelines discard the dict `callback_on_step_end` returns, so callback edits to `latents` are lost.",
strict=True,
)
class PRXPixelPipelineTesterConfig(BasePipelineTesterConfig):
# PRXPixelPipeline is standalone: it inherits from DiffusionPipeline (not PRXPipeline) and always has its own
# image_processor, so it denoises raw RGB in pixel space and supports output_type="pil"/"np" without a VAE.
pipeline_class = PRXPixelPipeline
params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_params = frozenset(["prompt", "negative_prompt", "num_images_per_prompt"])
test_xformers_attention = False
test_layerwise_casting = True
test_group_offloading = True
required_input_params_in_call_signature = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs"}
batch_input_params = frozenset(["prompt", "negative_prompt", "num_images_per_prompt"])
output_shape = (3, 32, 32)
def get_dummy_components(self):
torch.manual_seed(0)
@@ -64,108 +72,75 @@ class PRXPixelPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
"prompt_max_tokens": 16,
}
def get_dummy_inputs(self, device, seed=0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator(device=device).manual_seed(seed)
def get_dummy_inputs(self):
return {
"prompt": "",
"negative_prompt": "",
"generator": generator,
"generator": self.get_generator(0),
"num_inference_steps": 2,
"guidance_scale": 1.0,
"height": 32,
"width": 32,
# Pixel-space PRX has no VAE and returns raw (C, H, W) tensors for output_type="pt". The generic
# PipelineTesterMixin tests compare these tensors directly, so default to "pt" here; the PIL/np default
# path is exercised explicitly in test_inference and test_inference_pil_and_np_output.
# path is exercised explicitly in test_inference_pil_and_np_output.
"output_type": "pt",
# 32px is not in the 1024 aspect-ratio bins, so binning must be disabled for these tiny fast tests.
"use_resolution_binning": False,
}
def _build_pipe(self, device="cpu"):
components = self.get_dummy_components()
pipe = PRXPixelPipeline(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
return pipe
def test_inference(self):
device = "cpu"
pipe = self._build_pipe(device)
class TestPRXPixelPipeline(PRXPixelPipelineTesterConfig, PipelineTesterMixin):
@CALLBACK_OUTPUTS_IGNORED
def test_callback_inputs(self):
super().test_callback_inputs()
def test_pixel_space_has_no_vae(self):
# Pixel space: vae_scale_factor is always 1, and the pipeline always carries an image processor
# so postprocessing (and the default output_type="pil") works without any VAE.
self.assertEqual(pipe.vae_scale_factor, 1)
self.assertIsNotNone(pipe.image_processor)
pipe = self.get_pipeline()
# Default output is PIL (no VAE needed: the image processor denormalizes the denoised pixels directly).
inputs = self.get_dummy_inputs(device)
inputs.pop("output_type") # default is "pil"
images = pipe(**inputs).images
self.assertEqual(len(images), 1)
self.assertEqual(images[0].size, (32, 32))
# Raw "pt" output is the denoised RGB tensor at the requested resolution.
inputs = self.get_dummy_inputs(device)
image = pipe(**inputs)[0]
generated_image = image[0]
self.assertEqual(generated_image.shape, (3, 32, 32))
expected_image = torch.zeros(3, 32, 32)
max_diff = np.abs(generated_image.cpu().numpy() - expected_image.numpy()).max()
self.assertLessEqual(max_diff, 1e10)
assert pipe.vae_scale_factor == 1
assert pipe.image_processor is not None
def test_inference_batch(self):
device = "cpu"
pipe = self._build_pipe(device)
pipe = self.get_pipeline()
inputs = self.get_dummy_inputs(device)
inputs["prompt"] = ["", ""]
inputs["negative_prompt"] = ["", ""]
image = pipe(**inputs)[0]
image = self.run_pipe(pipe, prompt=["", ""], negative_prompt=["", ""])
self.assertEqual(image.shape[0], 2)
self.assertEqual(tuple(image.shape[1:]), (3, 32, 32))
assert image.shape[0] == 2
assert tuple(image.shape[1:]) == self.output_shape
def test_inference_with_cfg(self):
device = "cpu"
pipe = self._build_pipe(device)
pipe = self.get_pipeline()
# CFG off.
inputs = self.get_dummy_inputs(device)
inputs["guidance_scale"] = 1.0
out_no_cfg = pipe(**inputs)[0]
self.assertFalse(pipe.do_classifier_free_guidance)
self.assertEqual(out_no_cfg[0].shape, (3, 32, 32))
out_no_cfg = self.run_pipe(pipe, guidance_scale=1.0)
assert not pipe.do_classifier_free_guidance
assert out_no_cfg[0].shape == self.output_shape
# CFG on.
inputs = self.get_dummy_inputs(device)
inputs["guidance_scale"] = 5.0
out_cfg = pipe(**inputs)[0]
self.assertTrue(pipe.do_classifier_free_guidance)
self.assertEqual(out_cfg[0].shape, (3, 32, 32))
out_cfg = self.run_pipe(pipe, guidance_scale=5.0)
assert pipe.do_classifier_free_guidance
assert out_cfg[0].shape == self.output_shape
# Guidance should actually change the output.
max_diff = np.abs(out_no_cfg.cpu().numpy() - out_cfg.cpu().numpy()).max()
self.assertGreater(max_diff, 0.0)
assert not torch.allclose(out_no_cfg, out_cfg)
def test_inference_with_prompt_embeds(self):
device = "cpu"
pipe = self._build_pipe(device)
pipe = self.get_pipeline()
# Precompute embeddings via the public encode_prompt API (CFG on so we get negatives too).
prompt_embeds, prompt_attention_mask, negative_prompt_embeds, negative_prompt_attention_mask = (
pipe.encode_prompt(
prompt="a prompt",
device=device,
device=torch.device("cpu"),
do_classifier_free_guidance=True,
negative_prompt="",
)
)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs.pop("prompt")
inputs.pop("negative_prompt")
inputs["guidance_scale"] = 5.0
@@ -175,31 +150,29 @@ class PRXPixelPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
inputs["negative_prompt_attention_mask"] = negative_prompt_attention_mask
image = pipe(**inputs)[0]
self.assertEqual(image[0].shape, (3, 32, 32))
assert image[0].shape == self.output_shape
def test_inference_pil_and_np_output(self):
# The default output_type="pil" must work without a VAE: the denoised pixels are denormalized
# directly by the image processor instead of being decoded.
device = "cpu"
pipe = self._build_pipe(device)
pipe = self.get_pipeline()
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs.pop("output_type") # default is "pil"
images = pipe(**inputs).images
self.assertEqual(len(images), 1)
self.assertEqual(images[0].size, (32, 32))
assert len(images) == 1
assert images[0].size == (32, 32)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs["output_type"] = "np"
images = pipe(**inputs).images
self.assertEqual(images.shape, (1, 32, 32, 3))
self.assertGreaterEqual(images.min(), 0.0)
self.assertLessEqual(images.max(), 1.0)
assert images.shape == (1, 32, 32, 3)
assert images.min() >= 0.0
assert images.max() <= 1.0
def test_non_multiple_size_raises(self):
# height/width must be divisible by vae_scale_factor * transformer patch_size; check_inputs must raise
# a clear ValueError instead of letting the transformer fail on an invalid reshape mid-denoising.
device = "cpu"
components = self.get_dummy_components()
torch.manual_seed(0)
components["transformer"] = PRXTransformer2DModel(
@@ -214,82 +187,45 @@ class PRXPixelPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
bottleneck_size=8,
resolution_embeds=True,
)
pipe = PRXPixelPipeline(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
pipe = self.get_pipeline(**components)
inputs = self.get_dummy_inputs(device)
inputs = self.get_dummy_inputs()
inputs["height"] = 31 # vae_scale_factor (1) * patch_size (2) = 2; 31 is not a multiple
with self.assertRaisesRegex(ValueError, "divisible"):
with pytest.raises(ValueError, match="divisible"):
pipe(**inputs)
def test_callback_inputs(self):
device = "cpu"
pipe = self._build_pipe(device)
self.assertTrue(
hasattr(pipe, "_callback_tensor_inputs"),
f" {PRXPixelPipeline} should have `_callback_tensor_inputs` that defines a list of tensor variables its"
" callback function can use as inputs",
)
def callback_inputs_subset(pipe, i, t, callback_kwargs):
for tensor_name in callback_kwargs.keys():
assert tensor_name in pipe._callback_tensor_inputs
return callback_kwargs
def callback_inputs_all(pipe, i, t, callback_kwargs):
for tensor_name in pipe._callback_tensor_inputs:
assert tensor_name in callback_kwargs
for tensor_name in callback_kwargs.keys():
assert tensor_name in pipe._callback_tensor_inputs
return callback_kwargs
inputs = self.get_dummy_inputs(device)
inputs["callback_on_step_end"] = callback_inputs_subset
inputs["callback_on_step_end_tensor_inputs"] = ["latents"]
_ = pipe(**inputs)[0]
inputs = self.get_dummy_inputs(device)
inputs["callback_on_step_end"] = callback_inputs_all
inputs["callback_on_step_end_tensor_inputs"] = pipe._callback_tensor_inputs
_ = pipe(**inputs)[0]
def test_attention_slicing_forward_pass(self, expected_max_diff=1e-3):
# Overridden: the mixin version calls assert_mean_pixel_difference, which assumes HWC image
# arrays. Pixel-space PRX has no VAE; compare raw (C, H, W) tensors directly ("pt") instead of
# going through PIL.
if not self.test_attention_slicing:
return
# Run on CPU: sliced attention is compared against a full-attention run of the same pipeline.
pipe = self.get_pipeline()
components = self.get_dummy_components()
pipe = self.pipeline_class(**components)
for component in pipe.components.values():
if hasattr(component, "set_default_attn_processor"):
component.set_default_attn_processor()
pipe.to("cpu")
pipe.set_progress_bar_config(disable=None)
def to_np_local(tensor):
if isinstance(tensor, torch.Tensor):
return tensor.detach().cpu().numpy()
return tensor
generator_device = "cpu"
inputs = self.get_dummy_inputs(generator_device)
output_without_slicing = pipe(**inputs)[0]
output_without_slicing = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=1)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing1 = pipe(**inputs)[0]
output_with_slicing_1 = self.run_pipe(pipe)
pipe.enable_attention_slicing(slice_size=2)
inputs = self.get_dummy_inputs(generator_device)
output_with_slicing2 = pipe(**inputs)[0]
output_with_slicing_2 = self.run_pipe(pipe)
max_diff1 = np.abs(to_np_local(output_with_slicing1) - to_np_local(output_without_slicing)).max()
max_diff2 = np.abs(to_np_local(output_with_slicing2) - to_np_local(output_without_slicing)).max()
self.assertLess(max(max_diff1, max_diff2), expected_max_diff)
assert_tensors_close(
output_with_slicing_1,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=1) changed the output.",
)
assert_tensors_close(
output_with_slicing_2,
output_without_slicing,
atol=expected_max_diff,
msg="Attention slicing (slice_size=2) changed the output.",
)
@unittest.skip("Slow original-vs-diffusers parity test is optional and intentionally skipped for fast CI.")
def test_prx_pixel_original_parity(self):
pass
def test_encode_prompt_works_in_isolation(self):
extra_required_param_value_dict = {
"device": torch.device(torch_device).type,
"do_classifier_free_guidance": self.get_dummy_inputs().get("guidance_scale", 1.0) > 1.0,
}
return super().test_encode_prompt_works_in_isolation(extra_required_param_value_dict)
class TestPRXPixelPipelineMemory(PRXPixelPipelineTesterConfig, MemoryTesterMixin):
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the PRXPixel pipeline."""

View File

@@ -7,6 +7,7 @@ from .cache import (
TaylorSeerCacheTesterMixin,
)
from .common import BasePipelineTesterConfig, PipelineTesterMixin
from .from_pipe import FromPipeTesterMixin
from .ip_adapter import IPAdapterTesterMixin
from .lora import LoraMemoryTesterMixin, LoraTesterMixin, UNetLoraTesterMixin
from .memory import (
@@ -26,6 +27,7 @@ from .utils import (
__all__ = [
"BasePipelineTesterConfig",
"PipelineTesterMixin",
"FromPipeTesterMixin",
"IPAdapterTesterMixin",
"LoraTesterMixin",
"LoraMemoryTesterMixin",

View File

@@ -0,0 +1,208 @@
# coding=utf-8
# Copyright 2026 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 inspect
from diffusers import KolorsPipeline, StableDiffusionPipeline, StableDiffusionXLPipeline
from ...testing_utils import (
assert_tensors_close,
require_accelerate_version_greater,
require_accelerator,
torch_device,
)
from .common import BasePipelineOutputMixin
# The repo and constructor kwargs used to build the "original" pipeline a variant is derived from, keyed by that
# original pipeline class.
ORIGINAL_PIPELINE_REPOS = {
StableDiffusionPipeline: ("hf-internal-testing/tiny-stable-diffusion-torch", {"requires_safety_checker": False}),
StableDiffusionXLPipeline: (
"hf-internal-testing/tiny-stable-diffusion-xl-pipe",
{"requires_aesthetics_score": True, "force_zeros_for_empty_prompt": False},
),
KolorsPipeline: ("hf-internal-testing/tiny-kolors-pipe", {"force_zeros_for_empty_prompt": False}),
}
class FromPipeTesterMixin(BasePipelineOutputMixin):
"""`DiffusionPipeline.from_pipe` tests for pipelines that are variants of an existing one.
Composed with `BasePipelineTesterConfig`, which supplies `pipeline_class`, `get_dummy_components()` and
`get_dummy_inputs()`. The pytest-style successor of `PipelineFromPipeTesterMixin` in
`tests/pipelines/test_pipelines_common.py`.
"""
# Set on the test class to pull the original pipeline from a repo other than the default for its class.
original_pipeline_repo = None
@property
def original_pipeline_class(self):
"""The pipeline this one is a variant of — the source `from_pipe` is expected to round-trip through."""
name = self.pipeline_class.__name__.lower()
if "xl" in name:
return StableDiffusionXLPipeline
elif "kolors" in name:
return KolorsPipeline
return StableDiffusionPipeline
def get_dummy_inputs_pipe(self):
inputs = self.get_dummy_inputs()
inputs["return_dict"] = False
return inputs
def get_dummy_inputs_for_pipe_original(self):
"""The dummy inputs, restricted to the parameters the original pipeline's `__call__` accepts."""
original_call_params = set(inspect.signature(self.original_pipeline_class.__call__).parameters.keys())
return {k: v for k, v in self.get_dummy_inputs_pipe().items() if k in original_call_params}
def _split_components(self, components):
"""Split the given components into what the original pipeline expects and what only this one does."""
original_expected_modules, _ = self.original_pipeline_class._get_signature_keys(self.original_pipeline_class)
# components of this pipeline that the original one also expects
original_pipe_components = {}
# components this pipeline doesn't have, but the original one expects
original_pipe_additional_components = {}
# components this pipeline has, but the original one doesn't expect
current_pipe_additional_components = {}
for name, component in components.items():
if name in original_expected_modules:
original_pipe_components[name] = component
else:
current_pipe_additional_components[name] = component
for name in original_expected_modules:
if name not in original_pipe_components:
if name in self.original_pipeline_class._optional_components:
original_pipe_additional_components[name] = None
else:
raise ValueError(f"missing required module for {self.original_pipeline_class.__name__}: {name}")
return (
{**original_pipe_components, **original_pipe_additional_components},
current_pipe_additional_components,
)
def _build_original_pipeline(self, components):
"""Build the original pipeline out of `components`, plus the components only this pipeline has.
Both pipelines are built from the *same* component instances — rebuilding them per pipeline would make the
comparison depend on `get_dummy_components()` being bit-identical across calls.
"""
original_components, current_pipe_additional_components = self._split_components(components)
pipe_original = self.original_pipeline_class(**original_components)
pipe_original.set_progress_bar_config(disable=None)
return pipe_original, current_pipe_additional_components
def test_from_pipe_consistent_config(self):
original_repo, original_kwargs = ORIGINAL_PIPELINE_REPOS[self.original_pipeline_class]
original_repo = self.original_pipeline_repo or original_repo
# create original_pipeline_class(sd/sdxl/kolors)
pipe_original = self.original_pipeline_class.from_pretrained(original_repo, **original_kwargs)
# original_pipeline_class -> pipeline_class
pipe_additional_components = {
name: component
for name, component in self.get_dummy_components().items()
if name not in pipe_original.components
}
pipe = self.pipeline_class.from_pipe(pipe_original, **pipe_additional_components)
# pipeline_class -> original_pipeline_class
original_pipe_additional_components = {}
for name, component in pipe_original.components.items():
if name not in pipe.components or not isinstance(component, pipe.components[name].__class__):
original_pipe_additional_components[name] = component
pipe_original_2 = self.original_pipeline_class.from_pipe(pipe, **original_pipe_additional_components)
# compare the config
original_config = {k: v for k, v in pipe_original.config.items() if not k.startswith("_")}
original_config_2 = {k: v for k, v in pipe_original_2.config.items() if not k.startswith("_")}
assert original_config_2 == original_config
def test_from_pipe_consistent_forward_pass(self, expected_max_diff=1e-3):
components = self.get_dummy_components()
pipe_original, current_pipe_additional_components = self._build_original_pipeline(components)
pipe_original.to(torch_device)
output_original = pipe_original(**self.get_dummy_inputs_for_pipe_original())[0]
# `from_pipe` must not repurpose the original pipeline's attention processors — PAG and friends install
# their own on the derived pipeline only.
original_attn_processor_types = {
name: {k: type(v) for k, v in component.attn_processors.items()}
for name, component in pipe_original.components.items()
if hasattr(component, "attn_processors")
}
pipe = self.get_pipeline(**components).to(torch_device)
output = pipe(**self.get_dummy_inputs_pipe())[0]
pipe_from_original = self.pipeline_class.from_pipe(pipe_original, **current_pipe_additional_components)
pipe_from_original.to(torch_device)
pipe_from_original.set_progress_bar_config(disable=None)
output_from_original = pipe_from_original(**self.get_dummy_inputs_pipe())[0]
assert_tensors_close(
output_from_original,
output,
atol=expected_max_diff,
msg="The outputs of the pipelines created with `from_pipe` and `__init__` are different.",
)
output_original_2 = pipe_original(**self.get_dummy_inputs_for_pipe_original())[0]
assert_tensors_close(
output_original_2,
output_original,
atol=expected_max_diff,
msg="`from_pipe` should not change the output of original pipeline.",
)
for name, expected_types in original_attn_processor_types.items():
component = pipe_original.components[name]
assert {k: type(v) for k, v in component.attn_processors.items()} == expected_types, (
f"`from_pipe` changed the attention processors of `{name}` in the original pipeline."
)
@require_accelerator
@require_accelerate_version_greater("0.14.0")
def test_from_pipe_consistent_forward_pass_cpu_offload(self, expected_max_diff=1e-3):
components = self.get_dummy_components()
# Build the original pipeline before running anything. Both pipelines share one scheduler object, and some
# `__init__`s edit that object in place: `StableDiffusionPipeline`, for one, rewrites a `steps_offset` of 0
# to 1. Building it later would put the two forward passes below on different schedules.
pipe_original, current_pipe_additional_components = self._build_original_pipeline(components)
pipe = self.get_pipeline(**components)
pipe.enable_model_cpu_offload(device=torch_device)
output = pipe(**self.get_dummy_inputs_pipe())[0]
pipe_from_original = self.pipeline_class.from_pipe(pipe_original, **current_pipe_additional_components)
pipe_from_original.set_progress_bar_config(disable=None)
pipe_from_original.enable_model_cpu_offload(device=torch_device)
output_from_original = pipe_from_original(**self.get_dummy_inputs_pipe())[0]
assert_tensors_close(
output_from_original,
output,
atol=expected_max_diff,
msg="The outputs of the pipelines created with `from_pipe` and `__init__` are different.",
)

View File

@@ -1018,6 +1018,25 @@ def export_to_gif(image: list[PIL.Image.Image], output_gif_path: str = None) ->
return output_gif_path
@contextmanager
def skip_if_no_cudnn_engine():
"""
Skip the enclosing test when cuDNN has no kernel for an op the pipeline runs.
cuDNN does not ship an engine for every (op, dtype, layout) combination, and the set it covers differs between
cuDNN builds and GPU architectures. When none applies it raises `RuntimeError: GET was unable to find an engine
to execute this computation` — for instance for Sana's depthwise `Conv2d` in bfloat16. That is a property of the
runner, not of the code under test, so tests that can hit it are skipped there rather than failed. Any other
`RuntimeError` propagates untouched.
"""
try:
yield
except RuntimeError as e:
if "unable to find an engine" not in str(e):
raise
pytest.skip(f"cuDNN has no engine for this computation on {torch_device}: {e}")
@contextmanager
def buffered_writer(raw_f):
f = io.BufferedWriter(raw_f)