1- import unittest
2-
3- import numpy as np
1+ import pytest
42import torch
53from PIL import Image
64from transformers import Qwen2TokenizerFast , Qwen3Config , Qwen3ForCausalLM
1210 Flux2Transformer2DModel ,
1311)
1412
15- from ...testing_utils import torch_device
16- from ..test_pipelines_common import PipelineTesterMixin , check_qkv_fused_layers_exist
13+ from ...testing_utils import assert_tensors_close , torch_device
14+ from ..testing_utils import (
15+ BasePipelineTesterConfig ,
16+ MemoryTesterMixin ,
17+ PipelineTesterMixin ,
18+ check_qkv_fused_layers_exist ,
19+ )
1720
1821
19- class Flux2KleinKVPipelineFastTests ( PipelineTesterMixin , unittest . TestCase ):
22+ class Flux2KleinKVPipelineTesterConfig ( BasePipelineTesterConfig ):
2023 pipeline_class = Flux2KleinKVPipeline
21- params = frozenset (["prompt" , "height" , "width" , "prompt_embeds" , "image" ])
22- batch_params = frozenset (["prompt" ])
23-
24- test_xformers_attention = False
25- test_layerwise_casting = True
26- test_group_offloading = True
24+ required_input_params_in_call_signature = frozenset (["prompt" , "height" , "width" , "prompt_embeds" , "image" ])
25+ batch_input_params = frozenset (["prompt" ])
2726
2827 def get_dummy_components (self , num_layers : int = 1 , num_single_layers : int = 1 ):
2928 torch .manual_seed (0 )
@@ -83,67 +82,70 @@ def get_dummy_components(self, num_layers: int = 1, num_single_layers: int = 1):
8382 "vae" : vae ,
8483 }
8584
86- def get_dummy_inputs (self , device , seed = 0 ):
87- if str (device ).startswith ("mps" ):
88- generator = torch .manual_seed (seed )
89- else :
90- generator = torch .Generator (device = "cpu" ).manual_seed (seed )
91-
85+ def get_dummy_inputs (self ):
9286 inputs = {
9387 "prompt" : "a dog is dancing" ,
9488 "image" : Image .new ("RGB" , (64 , 64 )),
95- "generator" : generator ,
89+ "generator" : self . get_generator ( 0 ) ,
9690 "num_inference_steps" : 2 ,
9791 "height" : 8 ,
9892 "width" : 8 ,
9993 "max_sequence_length" : 64 ,
100- "output_type" : "np" ,
10194 "text_encoder_out_layers" : (1 ,),
95+ # Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
96+ "output_type" : "pt" ,
10297 }
10398 return inputs
10499
100+
101+ class TestFlux2KleinKVPipeline (Flux2KleinKVPipelineTesterConfig , PipelineTesterMixin ):
105102 def test_fused_qkv_projections (self ):
106- device = "cpu" # ensure determinism for the device-dependent torch.Generator
107- components = self .get_dummy_components ()
108- pipe = self .pipeline_class (** components )
109- pipe = pipe .to (device )
110- pipe .set_progress_bar_config (disable = None )
103+ # Run on CPU to keep the slice comparisons deterministic.
104+ pipe = self .get_pipeline ()
111105
112- inputs = self .get_dummy_inputs (device )
106+ inputs = self .get_dummy_inputs ()
113107 image = pipe (** inputs ).images
114- original_image_slice = image [0 , - 3 : , - 3 :, - 1 ]
108+ original_image_slice = image [0 , - 1 , - 3 :, - 3 : ]
115109
116110 pipe .transformer .fuse_qkv_projections ()
117- self .assertTrue (
118- check_qkv_fused_layers_exist (pipe .transformer , ["to_qkv" ]),
119- ("Something wrong with the fused attention layers. Expected all the attention projections to be fused." ),
111+ assert check_qkv_fused_layers_exist (pipe .transformer , ["to_qkv" ]), (
112+ "Something wrong with the fused attention layers. Expected all the attention projections to be fused."
120113 )
121114
122- inputs = self .get_dummy_inputs (device )
115+ inputs = self .get_dummy_inputs ()
123116 image = pipe (** inputs ).images
124- image_slice_fused = image [0 , - 3 : , - 3 :, - 1 ]
117+ image_slice_fused = image [0 , - 1 , - 3 :, - 3 : ]
125118
126119 pipe .transformer .unfuse_qkv_projections ()
127- inputs = self .get_dummy_inputs (device )
120+ inputs = self .get_dummy_inputs ()
128121 image = pipe (** inputs ).images
129- image_slice_disabled = image [0 , - 3 :, - 3 :, - 1 ]
130-
131- self .assertTrue (
132- np .allclose (original_image_slice , image_slice_fused , atol = 1e-3 , rtol = 1e-3 ),
133- ("Fusion of QKV projections shouldn't affect the outputs." ),
122+ image_slice_disabled = image [0 , - 1 , - 3 :, - 3 :]
123+
124+ assert_tensors_close (
125+ original_image_slice ,
126+ image_slice_fused ,
127+ atol = 1e-3 ,
128+ rtol = 1e-3 ,
129+ msg = "Fusion of QKV projections shouldn't affect the outputs." ,
134130 )
135- self .assertTrue (
136- np .allclose (image_slice_fused , image_slice_disabled , atol = 1e-3 , rtol = 1e-3 ),
137- ("Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled." ),
131+ assert_tensors_close (
132+ image_slice_fused ,
133+ image_slice_disabled ,
134+ atol = 1e-3 ,
135+ rtol = 1e-3 ,
136+ msg = "Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled." ,
138137 )
139- self .assertTrue (
140- np .allclose (original_image_slice , image_slice_disabled , atol = 1e-2 , rtol = 1e-2 ),
141- ("Original outputs should match when fused QKV projections are disabled." ),
138+ assert_tensors_close (
139+ original_image_slice ,
140+ image_slice_disabled ,
141+ atol = 1e-2 ,
142+ rtol = 1e-2 ,
143+ msg = "Original outputs should match when fused QKV projections are disabled." ,
142144 )
143145
144146 def test_image_output_shape (self ):
145- pipe = self .pipeline_class ( ** self . get_dummy_components () ).to (torch_device )
146- inputs = self .get_dummy_inputs (torch_device )
147+ pipe = self .get_pipeline ( ).to (torch_device )
148+ inputs = self .get_dummy_inputs ()
147149
148150 height_width_pairs = [(32 , 32 ), (72 , 57 )]
149151 for height , width in height_width_pairs :
@@ -152,21 +154,22 @@ def test_image_output_shape(self):
152154
153155 inputs .update ({"height" : height , "width" : width })
154156 image = pipe (** inputs ).images [0 ]
155- output_height , output_width , _ = image .shape
156- self .assertEqual (
157- (output_height , output_width ),
158- (expected_height , expected_width ),
159- f"Output shape { image .shape } does not match expected shape { (expected_height , expected_width )} " ,
157+ _ , output_height , output_width = image .shape
158+ assert (output_height , output_width ) == (expected_height , expected_width ), (
159+ f"Output shape { image .shape } does not match expected shape { (expected_height , expected_width )} "
160160 )
161161
162162 def test_without_image (self ):
163- device = "cpu"
164- pipe = self .pipeline_class (** self .get_dummy_components ()).to (device )
165- inputs = self .get_dummy_inputs (device )
163+ pipe = self .get_pipeline ().to (torch_device )
164+ inputs = self .get_dummy_inputs ()
166165 del inputs ["image" ]
167166 image = pipe (** inputs ).images
168- self . assertEqual ( image .shape , (1 , 8 , 8 , 3 ) )
167+ assert image .shape == (1 , 3 , 8 , 8 )
169168
170- @unittest .skip ("Needs to be revisited" )
169+ @pytest . mark .skip ("Needs to be revisited" )
171170 def test_encode_prompt_works_in_isolation (self ):
172171 pass
172+
173+
174+ class TestFlux2KleinKVPipelineMemory (Flux2KleinKVPipelineTesterConfig , MemoryTesterMixin ):
175+ """Memory optimization tests (CPU offload, group offload, layerwise casting) for the Flux2 Klein KV pipeline."""
0 commit comments