1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15- import inspect
16- import unittest
1715
1816import numpy as np
1917import torch
2220
2321from diffusers import AutoencoderKLCogVideoX , CogVideoXTransformer3DModel , CogVideoXVideoToVideoPipeline , DDIMScheduler
2422
25- from ... testing_utils import enable_full_determinism , torch_device
26- from .. pipeline_params import TEXT_TO_IMAGE_BATCH_PARAMS , TEXT_TO_IMAGE_IMAGE_PARAMS , TEXT_TO_IMAGE_PARAMS
27- from .. test_pipelines_common import (
23+ from ..testing_utils import (
24+ BasePipelineTesterConfig ,
25+ MemoryTesterMixin ,
2826 PipelineTesterMixin ,
2927 check_qkv_fusion_matches_attn_procs_length ,
3028 check_qkv_fusion_processors_exist ,
31- to_np ,
3229)
3330
3431
35- enable_full_determinism ()
36-
37-
38- class CogVideoXVideoToVideoPipelineFastTests (PipelineTesterMixin , unittest .TestCase ):
32+ class CogVideoXVideoToVideoPipelineTesterConfig (BasePipelineTesterConfig ):
3933 pipeline_class = CogVideoXVideoToVideoPipeline
40- params = TEXT_TO_IMAGE_PARAMS - {"cross_attention_kwargs" }
41- batch_params = TEXT_TO_IMAGE_BATCH_PARAMS .union ({"video" })
42- image_params = TEXT_TO_IMAGE_IMAGE_PARAMS
43- image_latents_params = TEXT_TO_IMAGE_IMAGE_PARAMS
44- required_optional_params = frozenset (
45- [
46- "num_inference_steps" ,
47- "generator" ,
48- "latents" ,
49- "return_dict" ,
50- "callback_on_step_end" ,
51- "callback_on_step_end_tensor_inputs" ,
52- ]
34+ required_input_params_in_call_signature = frozenset (
35+ ["prompt" , "negative_prompt" , "height" , "width" , "guidance_scale" , "prompt_embeds" , "negative_prompt_embeds" ]
36+ )
37+ batch_input_params = frozenset (["prompt" , "video" ])
38+ # CogVideoX is a video pipeline: it exposes `num_videos_per_prompt`, not the base default `num_images_per_prompt`.
39+ optional_input_params = frozenset (
40+ ["num_inference_steps" , "num_videos_per_prompt" , "generator" , "latents" , "output_type" , "return_dict" ]
5341 )
54- test_xformers_attention = False
5542
5643 def get_dummy_components (self ):
5744 torch .manual_seed (0 )
@@ -112,21 +99,16 @@ def get_dummy_components(self):
11299 }
113100 return components
114101
115- def get_dummy_inputs (self , device , seed : int = 0 , num_frames : int = 8 ):
116- if str (device ).startswith ("mps" ):
117- generator = torch .manual_seed (seed )
118- else :
119- generator = torch .Generator (device = device ).manual_seed (seed )
120-
102+ def get_dummy_inputs (self ):
121103 video_height = 16
122104 video_width = 16
123- video = [Image .new ("RGB" , (video_width , video_height ))] * num_frames
105+ video = [Image .new ("RGB" , (video_width , video_height ))] * 8
124106
125107 inputs = {
126108 "video" : video ,
127109 "prompt" : "dance monkey" ,
128110 "negative_prompt" : "" ,
129- "generator" : generator ,
111+ "generator" : self . get_generator ( 0 ) ,
130112 "num_inference_steps" : 2 ,
131113 "strength" : 0.5 ,
132114 "guidance_scale" : 6.0 ,
@@ -138,135 +120,38 @@ def get_dummy_inputs(self, device, seed: int = 0, num_frames: int = 8):
138120 }
139121 return inputs
140122
141- def test_inference (self ):
142- device = "cpu"
143123
144- components = self . get_dummy_components ()
145- pipe = self . pipeline_class ( ** components )
146- pipe . to ( device )
147- pipe . set_progress_bar_config ( disable = None )
124+ class TestCogVideoXVideoToVideoPipeline ( CogVideoXVideoToVideoPipelineTesterConfig , PipelineTesterMixin ):
125+ def test_inference ( self ):
126+ # Run on CPU: the expected slice below is CPU-specific.
127+ pipe = self . get_pipeline ( )
148128
149- inputs = self .get_dummy_inputs (device )
129+ inputs = self .get_dummy_inputs ()
150130 video = pipe (** inputs ).frames
151131 generated_video = video [0 ]
132+ assert generated_video .shape == (8 , 3 , 16 , 16 )
152133
153- self .assertEqual (generated_video .shape , (8 , 3 , 16 , 16 ))
154- expected_video = torch .randn (8 , 3 , 16 , 16 )
155- max_diff = np .abs (generated_video - expected_video ).max ()
156- self .assertLessEqual (max_diff , 1e10 )
157-
158- def test_callback_inputs (self ):
159- sig = inspect .signature (self .pipeline_class .__call__ )
160- has_callback_tensor_inputs = "callback_on_step_end_tensor_inputs" in sig .parameters
161- has_callback_step_end = "callback_on_step_end" in sig .parameters
162-
163- if not (has_callback_tensor_inputs and has_callback_step_end ):
164- return
165-
166- components = self .get_dummy_components ()
167- pipe = self .pipeline_class (** components )
168- pipe = pipe .to (torch_device )
169- pipe .set_progress_bar_config (disable = None )
170- self .assertTrue (
171- hasattr (pipe , "_callback_tensor_inputs" ),
172- f" { self .pipeline_class } should have `_callback_tensor_inputs` that defines a list of tensor variables its callback function can use as inputs" ,
173- )
174-
175- def callback_inputs_subset (pipe , i , t , callback_kwargs ):
176- # iterate over callback args
177- for tensor_name , tensor_value in callback_kwargs .items ():
178- # check that we're only passing in allowed tensor inputs
179- assert tensor_name in pipe ._callback_tensor_inputs
180-
181- return callback_kwargs
182-
183- def callback_inputs_all (pipe , i , t , callback_kwargs ):
184- for tensor_name in pipe ._callback_tensor_inputs :
185- assert tensor_name in callback_kwargs
186-
187- # iterate over callback args
188- for tensor_name , tensor_value in callback_kwargs .items ():
189- # check that we're only passing in allowed tensor inputs
190- assert tensor_name in pipe ._callback_tensor_inputs
134+ # fmt: off
135+ expected_slice = torch .tensor ([0.5644 , 0.6029 , 0.6017 , 0.5937 , 0.5991 , 0.5907 , 0.6141 , 0.5340 , 0.3184 , 0.4219 , 0.4406 , 0.4330 , 0.4692 , 0.4547 , 0.4562 , 0.5092 ])
136+ # fmt: on
191137
192- return callback_kwargs
193-
194- inputs = self .get_dummy_inputs (torch_device )
195-
196- # Test passing in a subset
197- inputs ["callback_on_step_end" ] = callback_inputs_subset
198- inputs ["callback_on_step_end_tensor_inputs" ] = ["latents" ]
199- output = pipe (** inputs )[0 ]
200-
201- # Test passing in a everything
202- inputs ["callback_on_step_end" ] = callback_inputs_all
203- inputs ["callback_on_step_end_tensor_inputs" ] = pipe ._callback_tensor_inputs
204- output = pipe (** inputs )[0 ]
205-
206- def callback_inputs_change_tensor (pipe , i , t , callback_kwargs ):
207- is_last = i == (pipe .num_timesteps - 1 )
208- if is_last :
209- callback_kwargs ["latents" ] = torch .zeros_like (callback_kwargs ["latents" ])
210- return callback_kwargs
211-
212- inputs ["callback_on_step_end" ] = callback_inputs_change_tensor
213- inputs ["callback_on_step_end_tensor_inputs" ] = pipe ._callback_tensor_inputs
214- output = pipe (** inputs )[0 ]
215- assert output .abs ().sum () < 1e10
138+ generated_slice = generated_video .flatten ()
139+ generated_slice = torch .cat ([generated_slice [:8 ], generated_slice [- 8 :]])
140+ assert torch .allclose (generated_slice , expected_slice , atol = 1e-3 )
216141
217142 def test_inference_batch_single_identical (self ):
218- self ._test_inference_batch_single_identical (batch_size = 3 , expected_max_diff = 1e-3 )
219-
220- def test_attention_slicing_forward_pass (
221- self , test_max_difference = True , test_mean_pixel_difference = True , expected_max_diff = 1e-3
222- ):
223- if not self .test_attention_slicing :
224- return
225-
226- components = self .get_dummy_components ()
227- pipe = self .pipeline_class (** components )
228- for component in pipe .components .values ():
229- if hasattr (component , "set_default_attn_processor" ):
230- component .set_default_attn_processor ()
231- pipe .to (torch_device )
232- pipe .set_progress_bar_config (disable = None )
233-
234- generator_device = "cpu"
235- inputs = self .get_dummy_inputs (generator_device )
236- output_without_slicing = pipe (** inputs )[0 ]
237-
238- pipe .enable_attention_slicing (slice_size = 1 )
239- inputs = self .get_dummy_inputs (generator_device )
240- output_with_slicing1 = pipe (** inputs )[0 ]
241-
242- pipe .enable_attention_slicing (slice_size = 2 )
243- inputs = self .get_dummy_inputs (generator_device )
244- output_with_slicing2 = pipe (** inputs )[0 ]
245-
246- if test_max_difference :
247- max_diff1 = np .abs (to_np (output_with_slicing1 ) - to_np (output_without_slicing )).max ()
248- max_diff2 = np .abs (to_np (output_with_slicing2 ) - to_np (output_without_slicing )).max ()
249- self .assertLess (
250- max (max_diff1 , max_diff2 ),
251- expected_max_diff ,
252- "Attention slicing should not affect the inference results" ,
253- )
143+ super ().test_inference_batch_single_identical (batch_size = 3 , expected_max_diff = 1e-3 )
254144
255145 def test_vae_tiling (self , expected_diff_max : float = 0.2 ):
256146 # Since VideoToVideo uses both encoder and decoder tiling, there seems to be much more numerical
257147 # difference. We seem to need a higher tolerance here...
258148 # TODO(aryan): Look into this more deeply
259149 expected_diff_max = 0.4
260150
261- generator_device = "cpu"
262- components = self .get_dummy_components ()
263-
264- pipe = self .pipeline_class (** components )
265- pipe .to ("cpu" )
266- pipe .set_progress_bar_config (disable = None )
151+ pipe = self .get_pipeline ()
267152
268153 # Without tiling
269- inputs = self .get_dummy_inputs (generator_device )
154+ inputs = self .get_dummy_inputs ()
270155 inputs ["height" ] = inputs ["width" ] = 128
271156 output_without_tiling = pipe (** inputs )[0 ]
272157
@@ -277,24 +162,18 @@ def test_vae_tiling(self, expected_diff_max: float = 0.2):
277162 tile_overlap_factor_height = 1 / 12 ,
278163 tile_overlap_factor_width = 1 / 12 ,
279164 )
280- inputs = self .get_dummy_inputs (generator_device )
165+ inputs = self .get_dummy_inputs ()
281166 inputs ["height" ] = inputs ["width" ] = 128
282167 output_with_tiling = pipe (** inputs )[0 ]
283168
284- self .assertLess (
285- (to_np (output_without_tiling ) - to_np (output_with_tiling )).max (),
286- expected_diff_max ,
287- "VAE tiling should not affect the inference results" ,
169+ assert (output_without_tiling - output_with_tiling ).max () < expected_diff_max , (
170+ "VAE tiling should not affect the inference results"
288171 )
289172
290173 def test_fused_qkv_projections (self ):
291- device = "cpu" # ensure determinism for the device-dependent torch.Generator
292- components = self .get_dummy_components ()
293- pipe = self .pipeline_class (** components )
294- pipe = pipe .to (device )
295- pipe .set_progress_bar_config (disable = None )
174+ pipe = self .get_pipeline ()
296175
297- inputs = self .get_dummy_inputs (device )
176+ inputs = self .get_dummy_inputs ()
298177 frames = pipe (** inputs ).frames # [B, F, C, H, W]
299178 original_image_slice = frames [0 , - 2 :, - 1 , - 3 :, - 3 :]
300179
@@ -306,12 +185,12 @@ def test_fused_qkv_projections(self):
306185 pipe .transformer , pipe .transformer .original_attn_processors
307186 ), "Something wrong with the attention processors concerning the fused QKV projections."
308187
309- inputs = self .get_dummy_inputs (device )
188+ inputs = self .get_dummy_inputs ()
310189 frames = pipe (** inputs ).frames
311190 image_slice_fused = frames [0 , - 2 :, - 1 , - 3 :, - 3 :]
312191
313192 pipe .transformer .unfuse_qkv_projections ()
314- inputs = self .get_dummy_inputs (device )
193+ inputs = self .get_dummy_inputs ()
315194 frames = pipe (** inputs ).frames
316195 image_slice_disabled = frames [0 , - 2 :, - 1 , - 3 :, - 3 :]
317196
@@ -324,3 +203,7 @@ def test_fused_qkv_projections(self):
324203 assert np .allclose (original_image_slice , image_slice_disabled , atol = 1e-2 , rtol = 1e-2 ), (
325204 "Original outputs should match when fused QKV projections are disabled."
326205 )
206+
207+
208+ class TestCogVideoXVideoToVideoPipelineMemory (CogVideoXVideoToVideoPipelineTesterConfig , MemoryTesterMixin ):
209+ pass
0 commit comments