11import random
2- import tempfile
3- import unittest
42
53import numpy as np
4+ import pytest
65import torch
76from PIL import Image
87from transformers import AutoConfig , AutoTokenizer , CLIPTextConfig , CLIPTextModel , CLIPTokenizer , T5EncoderModel
98
10- import diffusers
119from diffusers import AutoencoderKL , FlowMatchEulerDiscreteScheduler , FluxTransformer2DModel , VisualClozePipeline
12- from diffusers .utils import logging
1310
14- from ...testing_utils import (
15- CaptureLogger ,
16- enable_full_determinism ,
17- floats_tensor ,
18- require_accelerator ,
19- torch_device ,
20- )
21- from ..test_pipelines_common import PipelineTesterMixin , to_np
11+ from ...testing_utils import floats_tensor , torch_device
12+ from ..testing_utils import BasePipelineTesterConfig , MemoryTesterMixin , PipelineTesterMixin
2213
2314
24- enable_full_determinism ()
25-
26-
27- class VisualClozePipelineFastTests (unittest .TestCase , PipelineTesterMixin ):
15+ class VisualClozePipelineTesterConfig (BasePipelineTesterConfig ):
2816 pipeline_class = VisualClozePipeline
29- params = frozenset (
17+ required_input_params_in_call_signature = frozenset (
3018 [
3119 "task_prompt" ,
3220 "content_prompt" ,
@@ -38,10 +26,8 @@ class VisualClozePipelineFastTests(unittest.TestCase, PipelineTesterMixin):
3826 "upsampling_strength" ,
3927 ]
4028 )
41- batch_params = frozenset (["task_prompt" , "content_prompt" , "image" ])
42- test_xformers_attention = False
43- test_layerwise_casting = True
44- test_group_offloading = True
29+ batch_input_params = frozenset (["task_prompt" , "content_prompt" , "image" ])
30+ output_shape = (3 , 32 , 32 )
4531
4632 def get_dummy_components (self ):
4733 torch .manual_seed (0 )
@@ -109,7 +95,7 @@ def get_dummy_components(self):
10995 "resolution" : 32 ,
11096 }
11197
112- def get_dummy_inputs (self , device , seed = 0 ):
98+ def get_dummy_inputs (self , seed = 0 ):
11399 # Create example images to simulate the input format required by VisualCloze
114100 context_image = [
115101 Image .fromarray (floats_tensor ((32 , 32 , 3 ), rng = random .Random (seed ), scale = 255 ).numpy ().astype (np .uint8 ))
@@ -128,44 +114,41 @@ def get_dummy_inputs(self, device, seed=0):
128114 query_image , # Query image
129115 ]
130116
131- if str (device ).startswith ("mps" ):
132- generator = torch .manual_seed (seed )
133- else :
134- generator = torch .Generator (device = "cpu" ).manual_seed (seed )
135-
136117 inputs = {
137118 "task_prompt" : "Each row outlines a logical process, starting from [IMAGE1] gray-based depth map with detailed object contours, to achieve [IMAGE2] an image with flawless clarity." ,
138119 "content_prompt" : "A beautiful landscape with mountains and a lake" ,
139120 "image" : image ,
140- "generator" : generator ,
121+ "generator" : self . get_generator ( seed ) ,
141122 "num_inference_steps" : 2 ,
142123 "guidance_scale" : 5.0 ,
143124 "upsampling_height" : 32 ,
144125 "upsampling_width" : 32 ,
145126 "max_sequence_length" : 77 ,
146- "output_type" : "np" ,
147127 "upsampling_strength" : 0.4 ,
128+ # Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
129+ # Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
130+ "output_type" : "pt" ,
148131 }
149132 return inputs
150133
151- def test_visualcloze_different_prompts (self ):
152- pipe = self .pipeline_class (** self .get_dummy_components ()).to (torch_device )
153134
154- inputs = self .get_dummy_inputs (torch_device )
155- output_same_prompt = pipe (** inputs ).images [0 ]
135+ class TestVisualClozePipeline (VisualClozePipelineTesterConfig , PipelineTesterMixin ):
136+ def test_visualcloze_different_task_prompts (self , expected_min_diff = 1e-1 ):
137+ pipe = self .get_pipeline ().to (torch_device )
156138
157- inputs = self .get_dummy_inputs (torch_device )
158- inputs ["task_prompt" ] = "A different task to perform."
159- output_different_prompts = pipe (** inputs ).images [0 ]
139+ inputs = self .get_dummy_inputs ()
140+ output_original = pipe (** inputs ).images [0 ]
160141
161- max_diff = np .abs (output_same_prompt - output_different_prompts ).max ()
142+ inputs ["task_prompt" ] = "A different task description for image generation"
143+ output_different_task = pipe (** inputs ).images [0 ]
162144
163- # Outputs should be different
164- assert max_diff > 1e-6
145+ # Different task prompts should produce different outputs
146+ max_diff = (output_original - output_different_task ).abs ().max ()
147+ assert max_diff > expected_min_diff , "Outputs should be different for different task prompts."
165148
166149 def test_visualcloze_image_output_shape (self ):
167- pipe = self .pipeline_class ( ** self . get_dummy_components () ).to (torch_device )
168- inputs = self .get_dummy_inputs (torch_device )
150+ pipe = self .get_pipeline ( ).to (torch_device )
151+ inputs = self .get_dummy_inputs ()
169152
170153 height_width_pairs = [(32 , 32 ), (72 , 57 )]
171154 for height , width in height_width_pairs :
@@ -174,15 +157,14 @@ def test_visualcloze_image_output_shape(self):
174157
175158 inputs .update ({"upsampling_height" : height , "upsampling_width" : width })
176159 image = pipe (** inputs ).images [0 ]
177- output_height , output_width , _ = image .shape
178- assert (output_height , output_width ) == (expected_height , expected_width )
179-
180- def test_inference_batch_single_identical (self ):
181- self ._test_inference_batch_single_identical (expected_max_diff = 1e-3 )
160+ _ , output_height , output_width = image .shape
161+ assert (output_height , output_width ) == (expected_height , expected_width ), (
162+ f"Output shape { image .shape } does not match expected shape { (expected_height , expected_width )} "
163+ )
182164
183165 def test_upsampling_strength (self , expected_min_diff = 1e-1 ):
184- pipe = self .pipeline_class ( ** self . get_dummy_components () ).to (torch_device )
185- inputs = self .get_dummy_inputs (torch_device )
166+ pipe = self .get_pipeline ( ).to (torch_device )
167+ inputs = self .get_dummy_inputs ()
186168
187169 # Test different upsampling strengths
188170 inputs ["upsampling_strength" ] = 0.2
@@ -192,159 +174,19 @@ def test_upsampling_strength(self, expected_min_diff=1e-1):
192174 output_full_upsampling = pipe (** inputs ).images [0 ]
193175
194176 # Different upsampling strengths should produce different outputs
195- max_diff = np .abs (output_no_upsampling - output_full_upsampling ).max ()
196- assert max_diff > expected_min_diff
197-
198- def test_different_task_prompts (self , expected_min_diff = 1e-1 ):
199- pipe = self .pipeline_class (** self .get_dummy_components ()).to (torch_device )
200- inputs = self .get_dummy_inputs (torch_device )
201-
202- output_original = pipe (** inputs ).images [0 ]
203-
204- inputs ["task_prompt" ] = "A different task description for image generation"
205- output_different_task = pipe (** inputs ).images [0 ]
177+ max_diff = (output_no_upsampling - output_full_upsampling ).abs ().max ()
178+ assert max_diff > expected_min_diff , "Outputs should be different for different upsampling strengths."
206179
207- # Different task prompts should produce different outputs
208- max_diff = np .abs (output_original - output_different_task ).max ()
209- assert max_diff > expected_min_diff
180+ def test_inference_batch_single_identical (self , batch_size = 3 , expected_max_diff = 1e-3 ):
181+ super ().test_inference_batch_single_identical (batch_size = batch_size , expected_max_diff = expected_max_diff )
210182
211- @unittest .skip (
212- "Test not applicable because the pipeline being tested is a wrapper pipeline. CFG tests should be done on the inner pipelines."
183+ @pytest .mark .skip (
184+ "The pipeline being tested is a wrapper around two inner pipelines, so `guidance_scale` is not tracked on it. "
185+ "CFG tests should be done on the inner pipelines."
213186 )
214187 def test_callback_cfg (self ):
215188 pass
216189
217- def test_save_load_local (self , expected_max_difference = 1e-3 ):
218- components = self .get_dummy_components ()
219- pipe = self .pipeline_class (** components )
220- for component in pipe .components .values ():
221- if hasattr (component , "set_default_attn_processor" ):
222- component .set_default_attn_processor ()
223-
224- pipe .to (torch_device )
225- pipe .set_progress_bar_config (disable = None )
226-
227- inputs = self .get_dummy_inputs (torch_device )
228- output = pipe (** inputs )[0 ]
229-
230- logger = logging .get_logger ("diffusers.pipelines.pipeline_utils" )
231- logger .setLevel (diffusers .logging .INFO )
232-
233- with tempfile .TemporaryDirectory () as tmpdir :
234- pipe .save_pretrained (tmpdir , safe_serialization = False )
235-
236- with CaptureLogger (logger ) as cap_logger :
237- # NOTE: Resolution must be set to 32 for loading otherwise will lead to OOM on CI hardware
238- # This attribute is not serialized in the config of the pipeline
239- pipe_loaded = self .pipeline_class .from_pretrained (tmpdir , resolution = 32 )
240-
241- for component in pipe_loaded .components .values ():
242- if hasattr (component , "set_default_attn_processor" ):
243- component .set_default_attn_processor ()
244190
245- for name in pipe_loaded .components .keys ():
246- if name not in pipe_loaded ._optional_components :
247- assert name in str (cap_logger )
248-
249- pipe_loaded .to (torch_device )
250- pipe_loaded .set_progress_bar_config (disable = None )
251-
252- inputs = self .get_dummy_inputs (torch_device )
253- output_loaded = pipe_loaded (** inputs )[0 ]
254-
255- max_diff = np .abs (to_np (output ) - to_np (output_loaded )).max ()
256- self .assertLess (max_diff , expected_max_difference )
257-
258- def test_save_load_optional_components (self , expected_max_difference = 1e-4 ):
259- if not hasattr (self .pipeline_class , "_optional_components" ):
260- return
261- components = self .get_dummy_components ()
262- for key in components :
263- if "text_encoder" in key and hasattr (components [key ], "eval" ):
264- components [key ].eval ()
265- pipe = self .pipeline_class (** components )
266- for component in pipe .components .values ():
267- if hasattr (component , "set_default_attn_processor" ):
268- component .set_default_attn_processor ()
269- pipe .to (torch_device )
270- pipe .set_progress_bar_config (disable = None )
271-
272- # set all optional components to None
273- for optional_component in pipe ._optional_components :
274- setattr (pipe , optional_component , None )
275-
276- generator_device = "cpu"
277- inputs = self .get_dummy_inputs (generator_device )
278- torch .manual_seed (0 )
279- output = pipe (** inputs )[0 ]
280-
281- with tempfile .TemporaryDirectory () as tmpdir :
282- pipe .save_pretrained (tmpdir , safe_serialization = False )
283- # NOTE: Resolution must be set to 32 for loading otherwise will lead to OOM on CI hardware
284- # This attribute is not serialized in the config of the pipeline
285- pipe_loaded = self .pipeline_class .from_pretrained (tmpdir , resolution = 32 )
286- for component in pipe_loaded .components .values ():
287- if hasattr (component , "set_default_attn_processor" ):
288- component .set_default_attn_processor ()
289- pipe_loaded .to (torch_device )
290- pipe_loaded .set_progress_bar_config (disable = None )
291-
292- for optional_component in pipe ._optional_components :
293- self .assertTrue (
294- getattr (pipe_loaded , optional_component ) is None ,
295- f"`{ optional_component } ` did not stay set to None after loading." ,
296- )
297-
298- inputs = self .get_dummy_inputs (generator_device )
299- torch .manual_seed (0 )
300- output_loaded = pipe_loaded (** inputs )[0 ]
301-
302- max_diff = np .abs (to_np (output ) - to_np (output_loaded )).max ()
303- self .assertLess (max_diff , expected_max_difference )
304-
305- @unittest .skipIf (torch_device not in ["cuda" , "xpu" ], reason = "float16 requires CUDA or XPU" )
306- @require_accelerator
307- def test_save_load_float16 (self , expected_max_diff = 1e-2 ):
308- components = self .get_dummy_components ()
309- for name , module in components .items ():
310- if hasattr (module , "half" ):
311- components [name ] = module .to (torch_device ).half ()
312-
313- pipe = self .pipeline_class (** components )
314- for component in pipe .components .values ():
315- if hasattr (component , "set_default_attn_processor" ):
316- component .set_default_attn_processor ()
317- pipe .to (torch_device )
318- pipe .set_progress_bar_config (disable = None )
319-
320- inputs = self .get_dummy_inputs (torch_device )
321- output = pipe (** inputs )[0 ]
322-
323- with tempfile .TemporaryDirectory () as tmpdir :
324- pipe .save_pretrained (tmpdir )
325- # NOTE: Resolution must be set to 32 for loading otherwise will lead to OOM on CI hardware
326- # This attribute is not serialized in the config of the pipeline
327- pipe_loaded = self .pipeline_class .from_pretrained (tmpdir , torch_dtype = torch .float16 , resolution = 32 )
328- for component in pipe_loaded .components .values ():
329- if hasattr (component , "set_default_attn_processor" ):
330- component .set_default_attn_processor ()
331- pipe_loaded .to (torch_device )
332- pipe_loaded .set_progress_bar_config (disable = None )
333-
334- for name , component in pipe_loaded .components .items ():
335- if hasattr (component , "dtype" ):
336- self .assertTrue (
337- component .dtype == torch .float16 ,
338- f"`{ name } .dtype` switched from `float16` to { component .dtype } after loading." ,
339- )
340-
341- inputs = self .get_dummy_inputs (torch_device )
342- output_loaded = pipe_loaded (** inputs )[0 ]
343- max_diff = np .abs (to_np (output ) - to_np (output_loaded )).max ()
344- self .assertLess (
345- max_diff , expected_max_diff , "The output of the fp16 pipeline changed after saving and loading."
346- )
347-
348- @unittest .skip ("Test not supported." )
349- def test_pipeline_with_accelerator_device_map (self ):
350- pass
191+ class TestVisualClozePipelineMemory (VisualClozePipelineTesterConfig , MemoryTesterMixin ):
192+ """Memory optimization tests (CPU offload, group offload, layerwise casting) for the VisualCloze pipeline."""
0 commit comments