1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/tests/pipelines/testing_utils/ip_adapter.py
Sayak Paul 771779849b [tests] remove test_pipelines_common.py (#14669)
* migrate imports

* migrate stable diffusion adapter tests

* up

* move last remaining bits.

* simplify test policy wording

* move around

* move flux ip adapter testing stuff to flux module of test

* fix(ci): harden GitHub Actions workflows (#14669) (#14787)

fix(ci): harden workflow files flagged on #14669

Co-authored-by: hf-security-analysis[bot] <265538906+hf-security-analysis[bot]@users.noreply.github.com>

* clarify comment.

* remove name from batch_input

---------

Co-authored-by: hf-security-analysis[bot] <265538906+hf-security-analysis[bot]@users.noreply.github.com>
2026-10-06 11:43:52 +05:30

241 lines
11 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
import pytest
import torch
from diffusers.loaders import IPAdapterMixin
from ...testing_utils import assert_tensors_close, is_ip_adapter, torch_device
from .common import BasePipelineOutputMixin
@is_ip_adapter
class IPAdapterTesterMixin(BasePipelineOutputMixin):
"""IP-Adapter tests for UNet pipelines that load adapters through the standard `IPAdapterMixin` API.
Compose it with a `BasePipelineTesterConfig` subclass in its own test class, separate from the
`PipelineTesterMixin` one. Pipelines whose IP-Adapter API differs use a mixin of their own.
"""
def _get_dummy_image_embeds(self, cross_attention_dim: int = 32):
return torch.randn((2, 1, cross_attention_dim), device=torch_device)
def _get_dummy_faceid_image_embeds(self, cross_attention_dim: int = 32):
return torch.randn((2, 1, 1, cross_attention_dim), device=torch_device)
def _get_dummy_masks(self, input_size: int = 64):
masks = torch.zeros((1, 1, input_size, input_size), device=torch_device)
masks[0, :, :, : input_size // 2] = 1
return masks
def _get_ip_adapter_inputs(self):
inputs = self.get_dummy_inputs()
parameters = inspect.signature(self.pipeline_class.__call__).parameters
if "image" in parameters and "strength" in parameters:
inputs["num_inference_steps"] = 4
inputs["return_dict"] = False
return inputs
def _load_ip_adapters(self, pipe, num_adapters=1, faceid=False):
# The state dict builders are imported here rather than at module scope: they live 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.unets.test_models_unet_2d_condition import (
create_ip_adapter_faceid_state_dict,
create_ip_adapter_state_dict,
)
create_state_dict = create_ip_adapter_faceid_state_dict if faceid else create_ip_adapter_state_dict
state_dicts = [create_state_dict(pipe.unet) for _ in range(num_adapters)]
# Load through the pipeline's public IP-Adapter API. `image_encoder_folder=None` skips fetching a CLIP image
# encoder since these tests feed pre-computed `ip_adapter_image_embeds` directly.
pipe.load_ip_adapter(
state_dicts,
subfolder=[""] * num_adapters,
weight_name=[""] * num_adapters,
image_encoder_folder=None,
)
def test_pipeline_signature(self):
parameters = inspect.signature(self.pipeline_class.__call__).parameters
assert issubclass(self.pipeline_class, IPAdapterMixin)
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 test_ip_adapter(self, expected_max_diff: float = 1e-4):
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.
"""
pipe = self.get_pipeline().to(torch_device)
cross_attention_dim = pipe.unet.config.get("cross_attention_dim", 32)
# forward pass without ip adapter
output_without_adapter = pipe(**self._get_ip_adapter_inputs())[0]
# 1. Single IP-Adapter test cases
self._load_ip_adapters(pipe)
# forward pass with single ip adapter, but scale=0 which should have no effect
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)]
pipe.set_ip_adapter_scale(0.0)
output_without_adapter_scale = pipe(**inputs)[0]
# forward pass with single ip adapter, but with scale of adapter weights
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)]
pipe.set_ip_adapter_scale(42.0)
output_with_adapter_scale = pipe(**inputs)[0]
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
self._load_ip_adapters(pipe, num_adapters=2)
# forward pass with multi ip adapter, but scale=0 which should have no effect
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)] * 2
pipe.set_ip_adapter_scale([0.0, 0.0])
output_without_multi_adapter_scale = pipe(**inputs)[0]
# forward pass with multi ip adapter, but with scale of adapter weights
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)] * 2
pipe.set_ip_adapter_scale([42.0, 42.0])
output_with_multi_adapter_scale = pipe(**inputs)[0]
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"
)
def test_ip_adapter_cfg(self):
if "guidance_scale" not in inspect.signature(self.pipeline_class.__call__).parameters:
pytest.skip(
f"Skipping test because `guidance_scale` wasn't found in the args accepted in {self.pipeline_class}'s call."
)
pipe = self.get_pipeline().to(torch_device)
cross_attention_dim = pipe.unet.config.get("cross_attention_dim", 32)
self._load_ip_adapters(pipe)
pipe.set_ip_adapter_scale(1.0)
# forward pass with CFG not applied
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)[0].unsqueeze(0)]
inputs["guidance_scale"] = 1.0
out_no_cfg = pipe(**inputs)[0]
# forward pass with CFG applied
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)]
inputs["guidance_scale"] = 7.5
out_cfg = pipe(**inputs)[0]
assert out_cfg.shape == out_no_cfg.shape
def test_ip_adapter_masks(self, expected_max_diff: float = 1e-4):
pipe = self.get_pipeline().to(torch_device)
cross_attention_dim = pipe.unet.config.get("cross_attention_dim", 32)
sample_size = pipe.unet.config.get("sample_size", 32)
block_out_channels = pipe.vae.config.get("block_out_channels", [128, 256, 512, 512])
input_size = sample_size * (2 ** (len(block_out_channels) - 1))
# forward pass without ip adapter
output_without_adapter = pipe(**self._get_ip_adapter_inputs())[0]
self._load_ip_adapters(pipe)
# forward pass with single ip adapter and masks, but scale=0 which should have no effect
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)]
inputs["cross_attention_kwargs"] = {"ip_adapter_masks": [self._get_dummy_masks(input_size)]}
pipe.set_ip_adapter_scale(0.0)
output_without_adapter_scale = pipe(**inputs)[0]
# forward pass with single ip adapter and masks, but with scale of adapter weights
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_image_embeds(cross_attention_dim)]
inputs["cross_attention_kwargs"] = {"ip_adapter_masks": [self._get_dummy_masks(input_size)]}
pipe.set_ip_adapter_scale(42.0)
output_with_adapter_scale = pipe(**inputs)[0]
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-3, "Output with ip-adapter must be different from normal inference"
def test_ip_adapter_faceid(self, expected_max_diff: float = 1e-4):
pipe = self.get_pipeline().to(torch_device)
cross_attention_dim = pipe.unet.config.get("cross_attention_dim", 32)
# forward pass without ip adapter
output_without_adapter = pipe(**self._get_ip_adapter_inputs())[0]
self._load_ip_adapters(pipe, faceid=True)
# forward pass with single ip adapter, but scale=0 which should have no effect
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_faceid_image_embeds(cross_attention_dim)]
pipe.set_ip_adapter_scale(0.0)
output_without_adapter_scale = pipe(**inputs)[0]
# forward pass with single ip adapter, but with scale of adapter weights
inputs = self._get_ip_adapter_inputs()
inputs["ip_adapter_image_embeds"] = [self._get_dummy_faceid_image_embeds(cross_attention_dim)]
pipe.set_ip_adapter_scale(42.0)
output_with_adapter_scale = pipe(**inputs)[0]
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-3, "Output with ip-adapter must be different from normal inference"