1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15- import tempfile
16- import unittest
1715
18- import numpy as np
1916import torch
2017from PIL import Image
2118from transformers import (
2926
3027from diffusers import AutoencoderKLWan , FlowMatchEulerDiscreteScheduler , WanImageToVideoPipeline , WanTransformer3DModel
3128
32- from ...testing_utils import enable_full_determinism , torch_device
33- from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS , TEXT_TO_IMAGE_IMAGE_PARAMS , TEXT_TO_IMAGE_PARAMS
34- from ..test_pipelines_common import PipelineTesterMixin
29+ from ...testing_utils import assert_tensors_close , torch_device
30+ from ..testing_utils import BasePipelineTesterConfig , MemoryTesterMixin , PipelineTesterMixin
3531
3632
37- enable_full_determinism ()
38-
39-
40- class WanImageToVideoPipelineFastTests (PipelineTesterMixin , unittest .TestCase ):
33+ class WanImageToVideoPipelineTesterConfig (BasePipelineTesterConfig ):
4134 pipeline_class = WanImageToVideoPipeline
42- params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs" , "height" , "width" }
43- batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
44- image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
45- image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
46- required_optional_params = frozenset (
47- [
48- "num_inference_steps" ,
49- "generator" ,
50- "latents" ,
51- "return_dict" ,
52- "callback_on_step_end" ,
53- "callback_on_step_end_tensor_inputs" ,
54- ]
35+ required_input_params_in_call_signature = frozenset (
36+ ["image" , "prompt" , "negative_prompt" , "guidance_scale" , "prompt_embeds" , "negative_prompt_embeds" ]
37+ )
38+ batch_input_params = frozenset (["prompt" ])
39+ # Wan is a video pipeline: it exposes `num_videos_per_prompt`, not the base default `num_images_per_prompt`.
40+ optional_input_params = frozenset (
41+ ["num_inference_steps" , "num_videos_per_prompt" , "generator" , "latents" , "output_type" , "return_dict" ]
5542 )
56- test_xformers_attention = False
5743
5844 def get_dummy_components (self ):
5945 torch .manual_seed (0 )
@@ -104,7 +90,7 @@ def get_dummy_components(self):
10490 torch .manual_seed (0 )
10591 image_processor = CLIPImageProcessor (crop_size = 32 , size = 32 )
10692
107- components = {
93+ return {
10894 "transformer" : transformer ,
10995 "vae" : vae ,
11096 "scheduler" : scheduler ,
@@ -114,117 +100,87 @@ def get_dummy_components(self):
114100 "image_processor" : image_processor ,
115101 "transformer_2" : None ,
116102 }
117- return components
118103
119- def get_dummy_inputs (self , device , seed = 0 ):
120- if str (device ).startswith ("mps" ):
121- generator = torch .manual_seed (seed )
122- else :
123- generator = torch .Generator (device = device ).manual_seed (seed )
104+ def get_dummy_inputs (self ):
124105 image_height = 16
125106 image_width = 16
126107 image = Image .new ("RGB" , (image_width , image_height ))
127- inputs = {
108+ return {
128109 "image" : image ,
129110 "prompt" : "dance monkey" ,
130111 "negative_prompt" : "negative" , # TODO
131112 "height" : image_height ,
132113 "width" : image_width ,
133- "generator" : generator ,
114+ "generator" : self . get_generator ( 0 ) ,
134115 "num_inference_steps" : 2 ,
135116 "guidance_scale" : 6.0 ,
136117 "num_frames" : 9 ,
137118 "max_sequence_length" : 16 ,
119+ # Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
138120 "output_type" : "pt" ,
139121 }
140- return inputs
141122
142- def test_inference (self ):
143- device = "cpu"
144123
145- components = self . get_dummy_components ()
146- pipe = self . pipeline_class ( ** components )
147- pipe . to ( device )
148- pipe . set_progress_bar_config ( disable = None )
124+ class TestWanImageToVideoPipeline ( WanImageToVideoPipelineTesterConfig , PipelineTesterMixin ):
125+ def test_inference ( self ):
126+ # Run on CPU: the expected slice below is CPU-specific.
127+ pipe = self . get_pipeline ( )
149128
150- inputs = self .get_dummy_inputs (device )
129+ inputs = self .get_dummy_inputs ()
151130 video = pipe (** inputs ).frames
152131 generated_video = video [0 ]
153- self . assertEqual ( generated_video .shape , (9 , 3 , 16 , 16 ) )
132+ assert generated_video .shape == (9 , 3 , 16 , 16 )
154133
155134 # fmt: off
156135 expected_slice = torch .tensor ([0.4528 , 0.4525 , 0.4493 , 0.4537 , 0.4521 , 0.4532 , 0.4543 , 0.4536 , 0.5084 , 0.5252 , 0.5211 , 0.5120 , 0.5419 , 0.5355 , 0.5169 , 0.5213 ])
157136 # fmt: on
158137
159138 generated_slice = generated_video .flatten ()
160139 generated_slice = torch .cat ([generated_slice [:8 ], generated_slice [- 8 :]])
161- self .assertTrue (torch .allclose (generated_slice , expected_slice , atol = 1e-3 ))
162-
163- @unittest .skip ("Test not supported" )
164- def test_attention_slicing_forward_pass (self ):
165- pass
166-
167- @unittest .skip ("TODO: revisit failing as it requires a very high threshold to pass" )
168- def test_inference_batch_single_identical (self ):
169- pass
170-
171- # _optional_components include transformer, transformer_2 and image_encoder, image_processor, but only transformer_2 is optional for wan2.1 i2v pipeline
172- def test_save_load_optional_components (self , expected_max_difference = 1e-4 ):
173- optional_component = "transformer_2"
174-
175- components = self .get_dummy_components ()
176- components [optional_component ] = None
177- pipe = self .pipeline_class (** components )
178- for component in pipe .components .values ():
179- if hasattr (component , "set_default_attn_processor" ):
180- component .set_default_attn_processor ()
181- pipe .to (torch_device )
182- pipe .set_progress_bar_config (disable = None )
183-
184- generator_device = "cpu"
185- inputs = self .get_dummy_inputs (generator_device )
186- torch .manual_seed (0 )
140+ assert torch .allclose (generated_slice , expected_slice , atol = 1e-3 )
141+
142+ def test_save_load_optional_components (self , tmp_path , expected_max_difference = 1e-4 ):
143+ # `_optional_components` lists `transformer`, `transformer_2`, `image_encoder` and `image_processor`, but only
144+ # `transformer_2` is optional for this wan2.1 i2v pipeline. The base test nulls every optional component, which
145+ # would drop the required `transformer` and leave no denoiser, so restrict this to `transformer_2`.
146+ pipe = self .get_pipeline ().to (torch_device )
147+ pipe .transformer_2 = None
148+
149+ inputs = self .get_dummy_inputs ()
187150 output = pipe (** inputs )[0 ]
188151
189- with tempfile .TemporaryDirectory () as tmpdir :
190- pipe .save_pretrained (tmpdir , safe_serialization = False )
191- pipe_loaded = self .pipeline_class .from_pretrained (tmpdir )
192- for component in pipe_loaded .components .values ():
193- if hasattr (component , "set_default_attn_processor" ):
194- component .set_default_attn_processor ()
195- pipe_loaded .to (torch_device )
196- pipe_loaded .set_progress_bar_config (disable = None )
197-
198- self .assertTrue (
199- getattr (pipe_loaded , optional_component ) is None ,
200- f"`{ optional_component } ` did not stay set to None after loading." ,
201- )
152+ pipe .save_pretrained (tmp_path , safe_serialization = False )
153+ pipe_loaded = self .pipeline_class .from_pretrained (tmp_path )
154+ pipe_loaded .to (torch_device )
155+ pipe_loaded .set_progress_bar_config (disable = None )
202156
203- inputs = self .get_dummy_inputs (generator_device )
204- torch .manual_seed (0 )
157+ assert pipe_loaded .transformer_2 is None , "`transformer_2` did not stay set to None after loading."
158+
159+ inputs = self .get_dummy_inputs ()
205160 output_loaded = pipe_loaded (** inputs )[0 ]
206161
207- max_diff = np .abs (output .detach ().cpu ().numpy () - output_loaded .detach ().cpu ().numpy ()).max ()
208- self .assertLess (max_diff , expected_max_difference )
162+ assert_tensors_close (
163+ output_loaded ,
164+ output ,
165+ atol = expected_max_difference ,
166+ msg = "Output changed after dropping the optional component." ,
167+ )
168+
209169
170+ class TestWanImageToVideoPipelineMemory (WanImageToVideoPipelineTesterConfig , MemoryTesterMixin ):
171+ pass
210172
211- class WanFLFToVideoPipelineFastTests (PipelineTesterMixin , unittest .TestCase ):
173+
174+ class WanFLFToVideoPipelineTesterConfig (BasePipelineTesterConfig ):
212175 pipeline_class = WanImageToVideoPipeline
213- params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs" , "height" , "width" }
214- batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
215- image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
216- image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
217- required_optional_params = frozenset (
218- [
219- "num_inference_steps" ,
220- "generator" ,
221- "latents" ,
222- "return_dict" ,
223- "callback_on_step_end" ,
224- "callback_on_step_end_tensor_inputs" ,
225- ]
176+ required_input_params_in_call_signature = frozenset (
177+ ["image" , "prompt" , "negative_prompt" , "guidance_scale" , "prompt_embeds" , "negative_prompt_embeds" ]
178+ )
179+ batch_input_params = frozenset (["prompt" ])
180+ # Wan is a video pipeline: it exposes `num_videos_per_prompt`, not the base default `num_images_per_prompt`.
181+ optional_input_params = frozenset (
182+ ["num_inference_steps" , "num_videos_per_prompt" , "generator" , "latents" , "output_type" , "return_dict" ]
226183 )
227- test_xformers_attention = False
228184
229185 def get_dummy_components (self ):
230186 torch .manual_seed (0 )
@@ -276,7 +232,7 @@ def get_dummy_components(self):
276232 torch .manual_seed (0 )
277233 image_processor = CLIPImageProcessor (crop_size = 4 , size = 4 )
278234
279- components = {
235+ return {
280236 "transformer" : transformer ,
281237 "vae" : vae ,
282238 "scheduler" : scheduler ,
@@ -286,97 +242,74 @@ def get_dummy_components(self):
286242 "image_processor" : image_processor ,
287243 "transformer_2" : None ,
288244 }
289- return components
290245
291- def get_dummy_inputs (self , device , seed = 0 ):
292- if str (device ).startswith ("mps" ):
293- generator = torch .manual_seed (seed )
294- else :
295- generator = torch .Generator (device = device ).manual_seed (seed )
246+ def get_dummy_inputs (self ):
296247 image_height = 16
297248 image_width = 16
298249 image = Image .new ("RGB" , (image_width , image_height ))
299250 last_image = Image .new ("RGB" , (image_width , image_height ))
300- inputs = {
251+ return {
301252 "image" : image ,
302253 "last_image" : last_image ,
303254 "prompt" : "dance monkey" ,
304255 "negative_prompt" : "negative" ,
305256 "height" : image_height ,
306257 "width" : image_width ,
307- "generator" : generator ,
258+ "generator" : self . get_generator ( 0 ) ,
308259 "num_inference_steps" : 2 ,
309260 "guidance_scale" : 6.0 ,
310261 "num_frames" : 9 ,
311262 "max_sequence_length" : 16 ,
263+ # Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
312264 "output_type" : "pt" ,
313265 }
314- return inputs
315266
316- def test_inference (self ):
317- device = "cpu"
318267
319- components = self . get_dummy_components ()
320- pipe = self . pipeline_class ( ** components )
321- pipe . to ( device )
322- pipe . set_progress_bar_config ( disable = None )
268+ class TestWanFLFToVideoPipeline ( WanFLFToVideoPipelineTesterConfig , PipelineTesterMixin ):
269+ def test_inference ( self ):
270+ # Run on CPU: the expected slice below is CPU-specific.
271+ pipe = self . get_pipeline ( )
323272
324- inputs = self .get_dummy_inputs (device )
273+ inputs = self .get_dummy_inputs ()
325274 video = pipe (** inputs ).frames
326275 generated_video = video [0 ]
327- self . assertEqual ( generated_video .shape , (9 , 3 , 16 , 16 ) )
276+ assert generated_video .shape == (9 , 3 , 16 , 16 )
328277
329278 # fmt: off
330279 expected_slice = torch .tensor ([0.4525 , 0.4525 , 0.4497 , 0.4537 , 0.4520 , 0.4529 , 0.4540 , 0.4535 , 0.5157 , 0.5449 , 0.5201 , 0.5192 , 0.5398 , 0.5374 , 0.5162 , 0.5112 ])
331280 # fmt: on
332281
333282 generated_slice = generated_video .flatten ()
334283 generated_slice = torch .cat ([generated_slice [:8 ], generated_slice [- 8 :]])
335- self .assertTrue (torch .allclose (generated_slice , expected_slice , atol = 1e-3 ))
336-
337- @unittest .skip ("Test not supported" )
338- def test_attention_slicing_forward_pass (self ):
339- pass
340-
341- @unittest .skip ("TODO: revisit failing as it requires a very high threshold to pass" )
342- def test_inference_batch_single_identical (self ):
343- pass
344-
345- # _optional_components include transformer, transformer_2 and image_encoder, image_processor, but only transformer_2 is optional for wan2.1 FLFT2V pipeline
346- def test_save_load_optional_components (self , expected_max_difference = 1e-4 ):
347- optional_component = "transformer_2"
348-
349- components = self .get_dummy_components ()
350- components [optional_component ] = None
351- pipe = self .pipeline_class (** components )
352- for component in pipe .components .values ():
353- if hasattr (component , "set_default_attn_processor" ):
354- component .set_default_attn_processor ()
355- pipe .to (torch_device )
356- pipe .set_progress_bar_config (disable = None )
357-
358- generator_device = "cpu"
359- inputs = self .get_dummy_inputs (generator_device )
360- torch .manual_seed (0 )
284+ assert torch .allclose (generated_slice , expected_slice , atol = 1e-3 )
285+
286+ def test_save_load_optional_components (self , tmp_path , expected_max_difference = 1e-4 ):
287+ # `_optional_components` lists `transformer`, `transformer_2`, `image_encoder` and `image_processor`, but only
288+ # `transformer_2` is optional for this wan2.1 FLFT2V pipeline. The base test nulls every optional component,
289+ # which would drop the required `transformer` and leave no denoiser, so restrict this to `transformer_2`.
290+ pipe = self .get_pipeline ().to (torch_device )
291+ pipe .transformer_2 = None
292+
293+ inputs = self .get_dummy_inputs ()
361294 output = pipe (** inputs )[0 ]
362295
363- with tempfile .TemporaryDirectory () as tmpdir :
364- pipe .save_pretrained (tmpdir , safe_serialization = False )
365- pipe_loaded = self .pipeline_class .from_pretrained (tmpdir )
366- for component in pipe_loaded .components .values ():
367- if hasattr (component , "set_default_attn_processor" ):
368- component .set_default_attn_processor ()
369- pipe_loaded .to (torch_device )
370- pipe_loaded .set_progress_bar_config (disable = None )
371-
372- self .assertTrue (
373- getattr (pipe_loaded , optional_component ) is None ,
374- f"`{ optional_component } ` did not stay set to None after loading." ,
375- )
296+ pipe .save_pretrained (tmp_path , safe_serialization = False )
297+ pipe_loaded = self .pipeline_class .from_pretrained (tmp_path )
298+ pipe_loaded .to (torch_device )
299+ pipe_loaded .set_progress_bar_config (disable = None )
376300
377- inputs = self .get_dummy_inputs (generator_device )
378- torch .manual_seed (0 )
301+ assert pipe_loaded .transformer_2 is None , "`transformer_2` did not stay set to None after loading."
302+
303+ inputs = self .get_dummy_inputs ()
379304 output_loaded = pipe_loaded (** inputs )[0 ]
380305
381- max_diff = np .abs (output .detach ().cpu ().numpy () - output_loaded .detach ().cpu ().numpy ()).max ()
382- self .assertLess (max_diff , expected_max_difference )
306+ assert_tensors_close (
307+ output_loaded ,
308+ output ,
309+ atol = expected_max_difference ,
310+ msg = "Output changed after dropping the optional component." ,
311+ )
312+
313+
314+ class TestWanFLFToVideoPipelineMemory (WanFLFToVideoPipelineTesterConfig , MemoryTesterMixin ):
315+ pass
0 commit comments