Skip to content

Commit f83ba3b

Browse files
refactor flux2 klein kv pipeline tests to the new mixin structure (#14344)
Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
1 parent b16e3ce commit f83ba3b

1 file changed

Lines changed: 59 additions & 56 deletions

File tree

tests/pipelines/flux2/test_pipeline_flux2_klein_kv.py

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import unittest
2-
3-
import numpy as np
1+
import pytest
42
import torch
53
from PIL import Image
64
from transformers import Qwen2TokenizerFast, Qwen3Config, Qwen3ForCausalLM
@@ -12,18 +10,19 @@
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

Comments
 (0)