Skip to content

Commit d721801

Browse files
refactor wan image-to-video pipeline tests to the new mixin structure (#14228)
* refactor wan image-to-video pipeline tests to the new mixin structure * condense testing_utils imports --------- Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
1 parent 8f1d542 commit d721801

1 file changed

Lines changed: 94 additions & 161 deletions

File tree

tests/pipelines/wan/test_wan_image_to_video.py

Lines changed: 94 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
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
1916
import torch
2017
from PIL import Image
2118
from transformers import (
@@ -29,31 +26,20 @@
2926

3027
from 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

Comments
 (0)