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
16+ import pytest
1917import torch
2018from PIL import Image
2119from transformers import AutoConfig , AutoTokenizer , T5EncoderModel
2826 WanVACETransformer3DModel ,
2927)
3028
31- from ...testing_utils import enable_full_determinism , torch_device
32- from ..pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS , TEXT_TO_IMAGE_IMAGE_PARAMS , TEXT_TO_IMAGE_PARAMS
33- from ..test_pipelines_common import PipelineTesterMixin
29+ from ...testing_utils import assert_tensors_close , torch_device
30+ from ..testing_utils import BasePipelineTesterConfig , MemoryTesterMixin , PipelineTesterMixin
3431
3532
36- enable_full_determinism ()
37-
38-
39- class WanVACEPipelineFastTests (PipelineTesterMixin , unittest .TestCase ):
33+ class WanVACEPipelineTesterConfig (BasePipelineTesterConfig ):
4034 pipeline_class = WanVACEPipeline
41- params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs" }
42- batch_params = TEXT_TO_IMAGE_BATCH_PARAMS
43- image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
44- image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
45- required_optional_params = frozenset (
46- [
47- "num_inference_steps" ,
48- "generator" ,
49- "latents" ,
50- "return_dict" ,
51- "callback_on_step_end" ,
52- "callback_on_step_end_tensor_inputs" ,
53- ]
35+ required_input_params_in_call_signature = frozenset (
36+ ["prompt" , "negative_prompt" , "height" , "width" , "guidance_scale" , "prompt_embeds" , "negative_prompt_embeds" ]
37+ )
38+ batch_input_params = frozenset (["prompt" ])
39+ # WanVACE 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" ]
5442 )
55- test_xformers_attention = False
5643
5744 def get_dummy_components (self ):
5845 torch .manual_seed (0 )
@@ -88,147 +75,109 @@ def get_dummy_components(self):
8875 vace_in_channels = 96 ,
8976 )
9077
91- components = {
78+ return {
9279 "transformer" : transformer ,
9380 "vae" : vae ,
9481 "scheduler" : scheduler ,
9582 "text_encoder" : text_encoder ,
9683 "tokenizer" : tokenizer ,
9784 "transformer_2" : None ,
9885 }
99- return components
100-
101- def get_dummy_inputs (self , device , seed = 0 ):
102- if str (device ).startswith ("mps" ):
103- generator = torch .manual_seed (seed )
104- else :
105- generator = torch .Generator (device = device ).manual_seed (seed )
10686
87+ def get_dummy_inputs (self ):
10788 num_frames = 17
10889 height = 16
10990 width = 16
11091
11192 video = [Image .new ("RGB" , (height , width ))] * num_frames
11293 mask = [Image .new ("L" , (height , width ), 0 )] * num_frames
11394
114- inputs = {
95+ return {
11596 "video" : video ,
11697 "mask" : mask ,
11798 "prompt" : "dance monkey" ,
11899 "negative_prompt" : "negative" ,
119- "generator" : generator ,
100+ "generator" : self . get_generator ( 0 ) ,
120101 "num_inference_steps" : 2 ,
121102 "guidance_scale" : 6.0 ,
122103 "height" : 16 ,
123104 "width" : 16 ,
124105 "num_frames" : num_frames ,
125106 "max_sequence_length" : 16 ,
107+ # Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
126108 "output_type" : "pt" ,
127109 }
128- return inputs
129110
130- def test_inference (self ):
131- device = "cpu"
132111
133- components = self . get_dummy_components ()
134- pipe = self . pipeline_class ( ** components )
135- pipe . to ( device )
136- pipe . set_progress_bar_config ( disable = None )
112+ class TestWanVACEPipeline ( WanVACEPipelineTesterConfig , PipelineTesterMixin ):
113+ @ pytest . mark . skip ( reason = "Batching is not yet supported with this pipeline" )
114+ def test_inference_batch_consistent ( self ):
115+ pass
137116
138- inputs = self .get_dummy_inputs (device )
139- video = pipe (** inputs ).frames [0 ]
140- self .assertEqual (video .shape , (17 , 3 , 16 , 16 ))
117+ @pytest .mark .skip (reason = "Batching is not yet supported with this pipeline" )
118+ def test_inference_batch_single_identical (self ):
119+ pass
120+
121+ def test_inference (self ):
122+ # Run on CPU: the expected slice below is CPU-specific.
123+ pipe = self .get_pipeline ()
124+
125+ inputs = self .get_dummy_inputs ()
126+ video = pipe (** inputs ).frames
127+ generated_video = video [0 ]
128+ assert generated_video .shape == (17 , 3 , 16 , 16 )
141129
142130 # fmt: off
143- expected_slice = [0.4523 , 0.45198 , 0.44872 , 0.45326 , 0.45211 , 0.45258 , 0.45344 , 0.453 , 0.52431 , 0.52572 , 0.50701 , 0.5118 , 0.53717 , 0.53093 , 0.50557 , 0.51402 ]
131+ expected_slice = torch . tensor ( [0.4523 , 0.45198 , 0.44872 , 0.45326 , 0.45211 , 0.45258 , 0.45344 , 0.453 , 0.52431 , 0.52572 , 0.50701 , 0.5118 , 0.53717 , 0.53093 , 0.50557 , 0.51402 ])
144132 # fmt: on
145133
146- video_slice = video .flatten ()
147- video_slice = torch .cat ([video_slice [:8 ], video_slice [- 8 :]])
148- video_slice = [round (x , 5 ) for x in video_slice .tolist ()]
149- self .assertTrue (np .allclose (video_slice , expected_slice , atol = 1e-3 ))
134+ generated_slice = generated_video .flatten ()
135+ generated_slice = torch .cat ([generated_slice [:8 ], generated_slice [- 8 :]])
136+ assert torch .allclose (generated_slice , expected_slice , atol = 1e-3 )
150137
151138 def test_inference_with_single_reference_image (self ):
152- device = "cpu"
139+ # Run on CPU: the expected slice below is CPU-specific.
140+ pipe = self .get_pipeline ()
153141
154- components = self .get_dummy_components ()
155- pipe = self .pipeline_class (** components )
156- pipe .to (device )
157- pipe .set_progress_bar_config (disable = None )
158-
159- inputs = self .get_dummy_inputs (device )
142+ inputs = self .get_dummy_inputs ()
160143 inputs ["reference_images" ] = Image .new ("RGB" , (16 , 16 ))
161- video = pipe (** inputs ).frames [0 ]
162- self .assertEqual (video .shape , (17 , 3 , 16 , 16 ))
144+ video = pipe (** inputs ).frames
145+ generated_video = video [0 ]
146+ assert generated_video .shape == (17 , 3 , 16 , 16 )
163147
164148 # fmt: off
165- expected_slice = [0.45247 , 0.45214 , 0.44874 , 0.45314 , 0.45171 , 0.45299 , 0.45428 , 0.45317 , 0.51378 , 0.52658 , 0.53361 , 0.52303 , 0.46204 , 0.50435 , 0.52555 , 0.51342 ]
149+ expected_slice = torch . tensor ( [0.45247 , 0.45214 , 0.44874 , 0.45314 , 0.45171 , 0.45299 , 0.45428 , 0.45317 , 0.51378 , 0.52658 , 0.53361 , 0.52303 , 0.46204 , 0.50435 , 0.52555 , 0.51342 ])
166150 # fmt: on
167151
168- video_slice = video .flatten ()
169- video_slice = torch .cat ([video_slice [:8 ], video_slice [- 8 :]])
170- video_slice = [round (x , 5 ) for x in video_slice .tolist ()]
171- self .assertTrue (np .allclose (video_slice , expected_slice , atol = 1e-3 ))
152+ generated_slice = generated_video .flatten ()
153+ generated_slice = torch .cat ([generated_slice [:8 ], generated_slice [- 8 :]])
154+ assert torch .allclose (generated_slice , expected_slice , atol = 1e-3 )
172155
173156 def test_inference_with_multiple_reference_image (self ):
174- device = "cpu"
157+ # Run on CPU: the expected slice below is CPU-specific.
158+ pipe = self .get_pipeline ()
175159
176- components = self .get_dummy_components ()
177- pipe = self .pipeline_class (** components )
178- pipe .to (device )
179- pipe .set_progress_bar_config (disable = None )
180-
181- inputs = self .get_dummy_inputs (device )
160+ inputs = self .get_dummy_inputs ()
182161 inputs ["reference_images" ] = [[Image .new ("RGB" , (16 , 16 ))] * 2 ]
183- video = pipe (** inputs ).frames [0 ]
184- self .assertEqual (video .shape , (17 , 3 , 16 , 16 ))
162+ video = pipe (** inputs ).frames
163+ generated_video = video [0 ]
164+ assert generated_video .shape == (17 , 3 , 16 , 16 )
185165
186166 # fmt: off
187- expected_slice = [0.45321 , 0.45221 , 0.44818 , 0.45375 , 0.45268 , 0.4519 , 0.45271 , 0.45253 , 0.51244 , 0.52223 , 0.51253 , 0.51321 , 0.50743 , 0.51177 , 0.51626 , 0.50983 ]
167+ expected_slice = torch . tensor ( [0.45321 , 0.45221 , 0.44818 , 0.45375 , 0.45268 , 0.4519 , 0.45271 , 0.45253 , 0.51244 , 0.52223 , 0.51253 , 0.51321 , 0.50743 , 0.51177 , 0.51626 , 0.50983 ])
188168 # fmt: on
189169
190- video_slice = video .flatten ()
191- video_slice = torch .cat ([video_slice [:8 ], video_slice [- 8 :]])
192- video_slice = [round (x , 5 ) for x in video_slice .tolist ()]
193- self .assertTrue (np .allclose (video_slice , expected_slice , atol = 1e-3 ))
194-
195- @unittest .skip ("Test not supported" )
196- def test_attention_slicing_forward_pass (self ):
197- pass
198-
199- @unittest .skip ("Errors out because passing multiple prompts at once is not yet supported by this pipeline." )
200- def test_encode_prompt_works_in_isolation (self ):
201- pass
202-
203- @unittest .skip ("Batching is not yet supported with this pipeline" )
204- def test_inference_batch_consistent (self ):
205- pass
206-
207- @unittest .skip ("Batching is not yet supported with this pipeline" )
208- def test_inference_batch_single_identical (self ):
209- return super ().test_inference_batch_single_identical ()
210-
211- @unittest .skip (
212- "AutoencoderKLWan encoded latents are always in FP32. This test is not designed to handle mixed dtype inputs"
213- )
214- def test_float16_inference (self ):
215- pass
216-
217- @unittest .skip (
218- "AutoencoderKLWan encoded latents are always in FP32. This test is not designed to handle mixed dtype inputs"
219- )
220- def test_save_load_float16 (self ):
221- pass
170+ generated_slice = generated_video .flatten ()
171+ generated_slice = torch .cat ([generated_slice [:8 ], generated_slice [- 8 :]])
172+ assert torch .allclose (generated_slice , expected_slice , atol = 1e-3 )
222173
223174 def test_inference_with_only_transformer (self ):
224175 components = self .get_dummy_components ()
225176 components ["transformer_2" ] = None
226177 components ["boundary_ratio" ] = 0.0
227- pipe = self .pipeline_class (** components )
228- pipe .to (torch_device )
229- pipe .set_progress_bar_config (disable = None )
178+ pipe = self .get_pipeline (** components ).to (torch_device )
230179
231- inputs = self .get_dummy_inputs (torch_device )
180+ inputs = self .get_dummy_inputs ()
232181 video = pipe (** inputs ).frames [0 ]
233182 assert video .shape == (17 , 3 , 16 , 16 )
234183
@@ -244,56 +193,49 @@ def test_inference_with_only_transformer_2(self):
244193 )
245194
246195 components ["boundary_ratio" ] = 1.0
247- pipe = self .pipeline_class (** components )
248- pipe .to (torch_device )
249- pipe .set_progress_bar_config (disable = None )
196+ pipe = self .get_pipeline (** components ).to (torch_device )
250197
251- inputs = self .get_dummy_inputs (torch_device )
198+ inputs = self .get_dummy_inputs ()
252199 video = pipe (** inputs ).frames [0 ]
253200 assert video .shape == (17 , 3 , 16 , 16 )
254201
255- def test_save_load_optional_components (self , expected_max_difference = 1e-4 ):
256- optional_component = ["transformer" ]
257-
202+ def test_save_load_optional_components (self , tmp_path , expected_max_difference = 1e-4 ):
203+ # `_optional_components` lists both `transformer` and `transformer_2`. Here we drop the (optional)
204+ # `transformer` and denoise with `transformer_2` only, which needs `boundary_ratio=1.0` and a scheduler that
205+ # can run the low-noise stage on its own (FlowMatchEuler can't, since its starting timestep equals the
206+ # boundary).
258207 components = self .get_dummy_components ()
259208 components ["transformer_2" ] = components ["transformer" ]
260- # FlowMatchEulerDiscreteScheduler doesn't support running low noise only scheduler
261- # because starting timestep t == 1000 == boundary_timestep
209+ components ["transformer" ] = None
262210 components ["scheduler" ] = UniPCMultistepScheduler (
263211 prediction_type = "flow_prediction" , use_flow_sigmas = True , flow_shift = 3.0
264212 )
265- for component in optional_component :
266- components [component ] = None
267-
268213 components ["boundary_ratio" ] = 1.0
269214
270- pipe = self .pipeline_class (** components )
271- for component in pipe .components .values ():
272- if hasattr (component , "set_default_attn_processor" ):
273- component .set_default_attn_processor ()
274- pipe .to (torch_device )
275- pipe .set_progress_bar_config (disable = None )
215+ pipe = self .get_pipeline (** components ).to (torch_device )
276216
277- generator_device = "cpu"
278- inputs = self .get_dummy_inputs (generator_device )
217+ inputs = self .get_dummy_inputs ()
279218 torch .manual_seed (0 )
280219 output = pipe (** inputs )[0 ]
281220
282- with tempfile .TemporaryDirectory () as tmpdir :
283- pipe .save_pretrained (tmpdir , safe_serialization = False )
284- pipe_loaded = self .pipeline_class .from_pretrained (tmpdir )
285- for component in pipe_loaded .components .values ():
286- if hasattr (component , "set_default_attn_processor" ):
287- component .set_default_attn_processor ()
288- pipe_loaded .to (torch_device )
289- pipe_loaded .set_progress_bar_config (disable = None )
221+ pipe .save_pretrained (tmp_path , safe_serialization = False )
222+ pipe_loaded = self .pipeline_class .from_pretrained (tmp_path )
223+ pipe_loaded .to (torch_device )
224+ pipe_loaded .set_progress_bar_config (disable = None )
290225
291- for component in optional_component :
292- assert getattr (pipe_loaded , component ) is None , f"`{ component } ` did not stay set to None after loading."
226+ assert pipe_loaded .transformer is None , "`transformer` did not stay set to None after loading."
293227
294- inputs = self .get_dummy_inputs (generator_device )
228+ inputs = self .get_dummy_inputs ()
295229 torch .manual_seed (0 )
296230 output_loaded = pipe_loaded (** inputs )[0 ]
297231
298- max_diff = np .abs (output .detach ().cpu ().numpy () - output_loaded .detach ().cpu ().numpy ()).max ()
299- assert max_diff < expected_max_difference , "Outputs exceed expecpted maximum difference"
232+ assert_tensors_close (
233+ output_loaded ,
234+ output ,
235+ atol = expected_max_difference ,
236+ msg = "Output changed after dropping the optional component." ,
237+ )
238+
239+
240+ class TestWanVACEPipelineMemory (WanVACEPipelineTesterConfig , MemoryTesterMixin ):
241+ pass
0 commit comments