1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/tests/pipelines/flux/testing_utils.py
Sayak Paul a262497cd0 [tests] refactor c* pipeline tests. (#14626)
* refactor c* pipeline tests.

Migrate the remaining `tests/pipelines/` directories starting with C to the config class +
composable pytest mixins introduced in #14113: chroma, chronoedit, cogview3, consisid,
consistency_models, controlnet, controlnet_flux, controlnet_hunyuandit, controlnet_sd3 and
cosmos. cogvideo and cogview4 were already migrated.

Notable points beyond the mechanical conversion:

- Expected slices switch from the `"np"` layout (`[0, -3:, -3:, -1]`) to the `"pt"` one
  (`[0, -1, -3:, -3:]`); the values themselves are unchanged.
- The Cosmos `safety_checker` overrides (save/load, variants, dtype dict, encode_prompt)
  were duplicated across five files and now live in a shared
  `CosmosSafetyCheckerTesterMixin` in `tests/pipelines/cosmos/testing_utils.py`.
- chroma and controlnet_flux import `FluxIPAdapterTesterMixin` from `..flux.testing_utils`
  rather than the old `..test_pipelines_common` one.
- `PipelineTesterMixin.test_save_load_float16` now passes the non-deprecated `dtype=` to
  `from_pretrained` instead of `torch_dtype=`, which the Cosmos2.5 `from_pretrained`
  wrappers read when casting the safety checker they inject.
- Dropped along the way, matching #14619: `PipelineLatentTesterMixin`,
  `PipelineKarrasSchedulerTesterMixin`, the attention-slicing and xformers tests, and the
  `test_ip_adapter` overrides that only supplied an `expected_pipe_slice`.

`pytest tests/pipelines/{chroma,chronoedit,cogview3,consisid,consistency_models,controlnet,controlnet_flux,controlnet_hunyuandit,controlnet_sd3,cosmos}`
gives 14 failed, 1014 passed, 82 skipped. All 14 failures are in cosmos and reproduce
unchanged on main: `DummyCosmosSafetyChecker` is a parameter-less `ModelMixin` with a single
non-persistent buffer, so accelerate does not hook it for sequential offload and a reloaded
pipeline gets a fresh fp32 one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:13:24 +05:30

163 lines
8.4 KiB
Python

# 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 typing import Any
import torch
from diffusers.loaders import FluxIPAdapterMixin
from ...testing_utils import assert_tensors_close, is_ip_adapter, torch_device
from ..testing_utils.common import BasePipelineOutputMixin
@is_ip_adapter
class FluxIPAdapterTesterMixin(BasePipelineOutputMixin):
"""IP-Adapter tests shared by the Flux pipelines in this directory and by the other pipelines built on the
Flux IP-Adapter API (Chroma, Flux ControlNet).
Flux has its own IP-Adapter API (`FluxIPAdapterMixin`, image embeddings sized after the transformer's
`pooled_projection_dim`), so it doesn't reuse the Stable Diffusion `IPAdapterTesterMixin`. Compose it with a
`BasePipelineTesterConfig` subclass in its own test class, separate from the `PipelineTesterMixin` one.
"""
def test_pipeline_signature(self):
parameters = inspect.signature(self.pipeline_class.__call__).parameters
assert issubclass(self.pipeline_class, FluxIPAdapterMixin)
assert "ip_adapter_image" in parameters, (
"`ip_adapter_image` argument must be supported by the `__call__` method"
)
assert "ip_adapter_image_embeds" in parameters, (
"`ip_adapter_image_embeds` argument must be supported by the `__call__` method"
)
def _get_dummy_image_embeds(self, image_embed_dim: int = 768):
return torch.randn((1, 1, image_embed_dim), device=torch_device)
def _modify_inputs_for_ip_adapter_test(self, inputs: dict[str, Any]):
inputs["negative_prompt"] = ""
if "true_cfg_scale" in inspect.signature(self.pipeline_class.__call__).parameters:
inputs["true_cfg_scale"] = 4.0
# Request torch outputs so comparisons run on torch tensors directly (see `BasePipelineTesterConfig`).
inputs["output_type"] = "pt"
inputs["return_dict"] = False
return inputs
def test_ip_adapter(self, expected_max_diff: float = 1e-4, expected_pipe_slice=None):
r"""Tests for IP-Adapter.
The following scenarios are tested:
- Single IP-Adapter with scale=0 should produce same output as no IP-Adapter.
- Multi IP-Adapter with scale=0 should produce same output as no IP-Adapter.
- Single IP-Adapter with scale!=0 should produce different output compared to no IP-Adapter.
- Multi IP-Adapter with scale!=0 should produce different output compared to no IP-Adapter.
"""
# The state dict builder is imported here rather than at module scope: it lives in a model test module
# that calls `enable_full_determinism()` on import, which would otherwise flip that global for every test
# collected alongside this directory.
from ...models.transformers.test_models_transformer_flux import create_flux_ip_adapter_state_dict
# Raising the tolerance for this test when it's run on a CPU because we compare against static slices and
# that can be shaky (with a VVVV low probability).
expected_max_diff = 9e-4 if torch_device == "cpu" else expected_max_diff
components = self.get_dummy_components()
pipe = self.get_pipeline(**components).to(torch_device)
image_embed_dim = (
pipe.transformer.config.pooled_projection_dim
if hasattr(pipe.transformer.config, "pooled_projection_dim")
else 768
)
# forward pass without ip adapter
inputs = self._modify_inputs_for_ip_adapter_test(self.get_dummy_inputs())
if expected_pipe_slice is None:
output_without_adapter = pipe(**inputs)[0]
else:
output_without_adapter = expected_pipe_slice
# 1. Single IP-Adapter test cases
adapter_state_dict = create_flux_ip_adapter_state_dict(pipe.transformer)
# Load through the pipeline's public IP-Adapter API. `image_encoder_pretrained_model_name_or_path=None`
# skips fetching a CLIP image encoder since we feed pre-computed `ip_adapter_image_embeds` directly.
pipe.load_ip_adapter(adapter_state_dict, weight_name="", image_encoder_pretrained_model_name_or_path=None)
# forward pass with single ip adapter, but scale=0 which should have no effect
inputs = self._modify_inputs_for_ip_adapter_test(self.get_dummy_inputs())
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)]
inputs["negative_ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)]
pipe.set_ip_adapter_scale(0.0)
output_without_adapter_scale = pipe(**inputs)[0]
if expected_pipe_slice is not None:
output_without_adapter_scale = output_without_adapter_scale[0, -3:, -3:, -1].flatten()
# forward pass with single ip adapter, but with scale of adapter weights
inputs = self._modify_inputs_for_ip_adapter_test(self.get_dummy_inputs())
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)]
inputs["negative_ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)]
pipe.set_ip_adapter_scale(42.0)
output_with_adapter_scale = pipe(**inputs)[0]
if expected_pipe_slice is not None:
output_with_adapter_scale = output_with_adapter_scale[0, -3:, -3:, -1].flatten()
assert_tensors_close(
output_without_adapter_scale,
output_without_adapter,
atol=expected_max_diff,
msg="Output without ip-adapter must be same as normal inference",
)
max_diff_with_adapter_scale = (output_with_adapter_scale - output_without_adapter).abs().max()
assert max_diff_with_adapter_scale > 1e-2, "Output with ip-adapter must be different from normal inference"
# 2. Multi IP-Adapter test cases
adapter_state_dict_1 = create_flux_ip_adapter_state_dict(pipe.transformer)
adapter_state_dict_2 = create_flux_ip_adapter_state_dict(pipe.transformer)
pipe.load_ip_adapter(
[adapter_state_dict_1, adapter_state_dict_2],
weight_name=["", ""],
image_encoder_pretrained_model_name_or_path=None,
)
# forward pass with multi ip adapter, but scale=0 which should have no effect
inputs = self._modify_inputs_for_ip_adapter_test(self.get_dummy_inputs())
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)] * 2
inputs["negative_ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)] * 2
pipe.set_ip_adapter_scale([0.0, 0.0])
output_without_multi_adapter_scale = pipe(**inputs)[0]
if expected_pipe_slice is not None:
output_without_multi_adapter_scale = output_without_multi_adapter_scale[0, -3:, -3:, -1].flatten()
# forward pass with multi ip adapter, but with scale of adapter weights
inputs = self._modify_inputs_for_ip_adapter_test(self.get_dummy_inputs())
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)] * 2
inputs["negative_ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(image_embed_dim)] * 2
pipe.set_ip_adapter_scale([42.0, 42.0])
output_with_multi_adapter_scale = pipe(**inputs)[0]
if expected_pipe_slice is not None:
output_with_multi_adapter_scale = output_with_multi_adapter_scale[0, -3:, -3:, -1].flatten()
assert_tensors_close(
output_without_multi_adapter_scale,
output_without_adapter,
atol=expected_max_diff,
msg="Output without multi-ip-adapter must be same as normal inference",
)
max_diff_with_multi_adapter_scale = (output_with_multi_adapter_scale - output_without_adapter).abs().max()
assert max_diff_with_multi_adapter_scale > 1e-2, (
"Output with multi-ip-adapter scale must be different from normal inference"
)