Skip to content

Commit 26ae69b

Browse files
committed
start
1 parent fd823e8 commit 26ae69b

11 files changed

Lines changed: 3269 additions & 240 deletions

pipeline_testing_refactor_plan.md

Lines changed: 895 additions & 0 deletions
Large diffs are not rendered by default.

tests/pipelines/flux/test_pipeline_flux.py

Lines changed: 75 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,44 +23,40 @@
2323
slow,
2424
torch_device,
2525
)
26-
from ..test_pipelines_common import (
26+
from ..testing_utils import (
27+
BasePipelineTesterConfig,
2728
FasterCacheTesterMixin,
2829
FirstBlockCacheTesterMixin,
2930
FluxIPAdapterTesterMixin,
3031
MagCacheTesterMixin,
32+
MemoryTesterMixin,
3133
PipelineTesterMixin,
3234
PyramidAttentionBroadcastTesterMixin,
3335
TaylorSeerCacheTesterMixin,
3436
check_qkv_fused_layers_exist,
3537
)
3638

3739

38-
class FluxPipelineFastTests(
39-
PipelineTesterMixin,
40-
FluxIPAdapterTesterMixin,
41-
PyramidAttentionBroadcastTesterMixin,
42-
FasterCacheTesterMixin,
43-
FirstBlockCacheTesterMixin,
44-
TaylorSeerCacheTesterMixin,
45-
MagCacheTesterMixin,
46-
unittest.TestCase,
47-
):
48-
pipeline_class = FluxPipeline
49-
params = frozenset(["prompt", "height", "width", "guidance_scale", "prompt_embeds", "pooled_prompt_embeds"])
50-
batch_params = frozenset(["prompt"])
40+
class FluxPipelineTesterConfig(BasePipelineTesterConfig):
41+
@property
42+
def pipeline_class(self):
43+
return FluxPipeline
5144

52-
# there is no xformers processor for Flux
53-
test_xformers_attention = False
54-
test_layerwise_casting = True
55-
test_group_offloading = True
45+
@property
46+
def params(self):
47+
return frozenset(["prompt", "height", "width", "guidance_scale", "prompt_embeds", "pooled_prompt_embeds"])
5648

57-
faster_cache_config = FasterCacheConfig(
58-
spatial_attention_block_skip_range=2,
59-
spatial_attention_timestep_skip_range=(-1, 901),
60-
unconditional_batch_skip_range=2,
61-
attention_weight_callback=lambda _: 0.5,
62-
is_guidance_distilled=True,
63-
)
49+
@property
50+
def batch_params(self):
51+
return frozenset(["prompt"])
52+
53+
@property
54+
def test_layerwise_casting(self):
55+
return True
56+
57+
@property
58+
def test_group_offloading(self):
59+
return True
6460

6561
def get_dummy_components(self, num_layers: int = 1, num_single_layers: int = 1):
6662
torch.manual_seed(0)
@@ -146,6 +142,8 @@ def get_dummy_inputs(self, device, seed=0):
146142
}
147143
return inputs
148144

145+
146+
class TestFluxPipeline(FluxPipelineTesterConfig, PipelineTesterMixin):
149147
def test_flux_different_prompts(self):
150148
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
151149

@@ -160,7 +158,7 @@ def test_flux_different_prompts(self):
160158

161159
# Outputs should be different here
162160
# For some reasons, they don't show large differences
163-
self.assertGreater(max_diff, 1e-6, "Outputs should be different for different prompts.")
161+
assert max_diff > 1e-6, "Outputs should be different for different prompts."
164162

165163
def test_fused_qkv_projections(self):
166164
device = "cpu" # ensure determinism for the device-dependent torch.Generator
@@ -176,9 +174,8 @@ def test_fused_qkv_projections(self):
176174
# TODO (sayakpaul): will refactor this once `fuse_qkv_projections()` has been added
177175
# to the pipeline level.
178176
pipe.transformer.fuse_qkv_projections()
179-
self.assertTrue(
180-
check_qkv_fused_layers_exist(pipe.transformer, ["to_qkv"]),
181-
("Something wrong with the fused attention layers. Expected all the attention projections to be fused."),
177+
assert check_qkv_fused_layers_exist(pipe.transformer, ["to_qkv"]), (
178+
"Something wrong with the fused attention layers. Expected all the attention projections to be fused."
182179
)
183180

184181
inputs = self.get_dummy_inputs(device)
@@ -190,17 +187,14 @@ def test_fused_qkv_projections(self):
190187
image = pipe(**inputs).images
191188
image_slice_disabled = image[0, -3:, -3:, -1]
192189

193-
self.assertTrue(
194-
np.allclose(original_image_slice, image_slice_fused, atol=1e-3, rtol=1e-3),
195-
("Fusion of QKV projections shouldn't affect the outputs."),
190+
assert np.allclose(original_image_slice, image_slice_fused, atol=1e-3, rtol=1e-3), (
191+
"Fusion of QKV projections shouldn't affect the outputs."
196192
)
197-
self.assertTrue(
198-
np.allclose(image_slice_fused, image_slice_disabled, atol=1e-3, rtol=1e-3),
199-
("Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled."),
193+
assert np.allclose(image_slice_fused, image_slice_disabled, atol=1e-3, rtol=1e-3), (
194+
"Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled."
200195
)
201-
self.assertTrue(
202-
np.allclose(original_image_slice, image_slice_disabled, atol=1e-2, rtol=1e-2),
203-
("Original outputs should match when fused QKV projections are disabled."),
196+
assert np.allclose(original_image_slice, image_slice_disabled, atol=1e-2, rtol=1e-2), (
197+
"Original outputs should match when fused QKV projections are disabled."
204198
)
205199

206200
def test_flux_image_output_shape(self):
@@ -215,10 +209,8 @@ def test_flux_image_output_shape(self):
215209
inputs.update({"height": height, "width": width})
216210
image = pipe(**inputs).images[0]
217211
output_height, output_width, _ = image.shape
218-
self.assertEqual(
219-
(output_height, output_width),
220-
(expected_height, expected_width),
221-
f"Output shape {image.shape} does not match expected shape {(expected_height, expected_width)}",
212+
assert (output_height, output_width) == (expected_height, expected_width), (
213+
f"Output shape {image.shape} does not match expected shape {(expected_height, expected_width)}"
222214
)
223215

224216
def test_flux_true_cfg(self):
@@ -230,11 +222,48 @@ def test_flux_true_cfg(self):
230222
inputs["negative_prompt"] = "bad quality"
231223
inputs["true_cfg_scale"] = 2.0
232224
true_cfg_out = pipe(**inputs, generator=torch.manual_seed(0)).images[0]
233-
self.assertFalse(
234-
np.allclose(no_true_cfg_out, true_cfg_out), "Outputs should be different when true_cfg_scale is set."
225+
assert not np.allclose(no_true_cfg_out, true_cfg_out), (
226+
"Outputs should be different when true_cfg_scale is set."
235227
)
236228

237229

230+
class TestFluxPipelineMemory(FluxPipelineTesterConfig, MemoryTesterMixin):
231+
"""Offload / device-map / group-offload / layerwise-casting tests for Flux."""
232+
233+
234+
class TestFluxPipelineIPAdapter(FluxPipelineTesterConfig, FluxIPAdapterTesterMixin):
235+
"""IP-Adapter tests for Flux."""
236+
237+
238+
class TestFluxPipelinePAB(FluxPipelineTesterConfig, PyramidAttentionBroadcastTesterMixin):
239+
"""Pyramid Attention Broadcast cache tests for Flux."""
240+
241+
242+
class TestFluxPipelineFasterCache(FluxPipelineTesterConfig, FasterCacheTesterMixin):
243+
"""FasterCache tests for Flux."""
244+
245+
# Flux is guidance distilled, so we set `is_guidance_distilled=True`.
246+
faster_cache_config = FasterCacheConfig(
247+
spatial_attention_block_skip_range=2,
248+
spatial_attention_timestep_skip_range=(-1, 901),
249+
unconditional_batch_skip_range=2,
250+
attention_weight_callback=lambda _: 0.5,
251+
is_guidance_distilled=True,
252+
)
253+
254+
255+
class TestFluxPipelineFirstBlockCache(FluxPipelineTesterConfig, FirstBlockCacheTesterMixin):
256+
"""FirstBlockCache tests for Flux."""
257+
258+
259+
class TestFluxPipelineTaylorSeerCache(FluxPipelineTesterConfig, TaylorSeerCacheTesterMixin):
260+
"""TaylorSeerCache tests for Flux."""
261+
262+
263+
class TestFluxPipelineMagCache(FluxPipelineTesterConfig, MagCacheTesterMixin):
264+
"""MagCache tests for Flux."""
265+
266+
238267
@nightly
239268
@require_big_accelerator
240269
class FluxPipelineSlowTests(unittest.TestCase):
@@ -293,9 +322,7 @@ def test_flux_inference(self):
293322
# fmt: on
294323

295324
max_diff = numpy_cosine_similarity_distance(expected_slice.flatten(), image_slice.flatten())
296-
self.assertLess(
297-
max_diff, 1e-4, f"Image slice is different from expected slice: {image_slice} != {expected_slice}"
298-
)
325+
assert max_diff < 1e-4, f"Image slice is different from expected slice: {image_slice} != {expected_slice}"
299326

300327

301328
@slow
@@ -373,6 +400,4 @@ def test_flux_ip_adapter_inference(self):
373400
# fmt: on
374401

375402
max_diff = numpy_cosine_similarity_distance(expected_slice.flatten(), image_slice.flatten())
376-
self.assertLess(
377-
max_diff, 1e-4, f"Image slice is different from expected slice: {image_slice} != {expected_slice}"
378-
)
403+
assert max_diff < 1e-4, f"Image slice is different from expected slice: {image_slice} != {expected_slice}"
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# coding=utf-8
2+
# Copyright 2025 HuggingFace Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import json
17+
import os
18+
import tempfile
19+
import unittest
20+
import uuid
21+
22+
import torch
23+
from huggingface_hub import ModelCard, delete_repo
24+
from huggingface_hub.utils import is_jinja_available
25+
from transformers import CLIPTextConfig, CLIPTextModel, CLIPTokenizer
26+
27+
from diffusers import (
28+
AutoencoderKL,
29+
DDIMScheduler,
30+
StableDiffusionPipeline,
31+
UNet2DConditionModel,
32+
)
33+
34+
from ..others.test_utils import TOKEN, USER, is_staging_test
35+
36+
37+
# Standalone, pipeline-agnostic Hub integration test. It does not compose the `BasePipelineTesterConfig`
38+
# fixtures (it builds its own fixed SD components) and relies on `@is_staging_test` (a `unittest.skip`-based
39+
# decorator), so it stays a `unittest.TestCase` rather than a config + mixin test.
40+
@is_staging_test
41+
class TestPipelinePushToHub(unittest.TestCase):
42+
identifier = uuid.uuid4()
43+
repo_id = f"test-pipeline-{identifier}"
44+
org_repo_id = f"valid_org/{repo_id}-org"
45+
46+
def get_pipeline_components(self):
47+
unet = UNet2DConditionModel(
48+
block_out_channels=(32, 64),
49+
layers_per_block=2,
50+
sample_size=32,
51+
in_channels=4,
52+
out_channels=4,
53+
down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"),
54+
up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"),
55+
cross_attention_dim=32,
56+
)
57+
58+
scheduler = DDIMScheduler(
59+
beta_start=0.00085,
60+
beta_end=0.012,
61+
beta_schedule="scaled_linear",
62+
clip_sample=False,
63+
set_alpha_to_one=False,
64+
)
65+
66+
vae = AutoencoderKL(
67+
block_out_channels=[32, 64],
68+
in_channels=3,
69+
out_channels=3,
70+
down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"],
71+
up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"],
72+
latent_channels=4,
73+
)
74+
75+
text_encoder_config = CLIPTextConfig(
76+
bos_token_id=0,
77+
eos_token_id=2,
78+
hidden_size=32,
79+
intermediate_size=37,
80+
layer_norm_eps=1e-05,
81+
num_attention_heads=4,
82+
num_hidden_layers=5,
83+
pad_token_id=1,
84+
vocab_size=1000,
85+
)
86+
text_encoder = CLIPTextModel(text_encoder_config)
87+
88+
with tempfile.TemporaryDirectory() as tmpdir:
89+
dummy_vocab = {"<|startoftext|>": 0, "<|endoftext|>": 1, "!": 2}
90+
vocab_path = os.path.join(tmpdir, "vocab.json")
91+
with open(vocab_path, "w") as f:
92+
json.dump(dummy_vocab, f)
93+
94+
merges = "Ġ t\nĠt h"
95+
merges_path = os.path.join(tmpdir, "merges.txt")
96+
with open(merges_path, "w") as f:
97+
f.writelines(merges)
98+
tokenizer = CLIPTokenizer(vocab_file=vocab_path, merges_file=merges_path)
99+
100+
components = {
101+
"unet": unet,
102+
"scheduler": scheduler,
103+
"vae": vae,
104+
"text_encoder": text_encoder,
105+
"tokenizer": tokenizer,
106+
"safety_checker": None,
107+
"feature_extractor": None,
108+
}
109+
return components
110+
111+
def test_push_to_hub(self):
112+
components = self.get_pipeline_components()
113+
pipeline = StableDiffusionPipeline(**components)
114+
pipeline.push_to_hub(self.repo_id, token=TOKEN)
115+
116+
new_model = UNet2DConditionModel.from_pretrained(f"{USER}/{self.repo_id}", subfolder="unet")
117+
unet = components["unet"]
118+
for p1, p2 in zip(unet.parameters(), new_model.parameters()):
119+
self.assertTrue(torch.equal(p1, p2))
120+
121+
# Push to hub via save_pretrained to a separate repo. Reusing `self.repo_id` after
122+
# deleting it makes the staging server's LFS GC reject the next commit with
123+
# "LFS pointer pointed to a file that does not exist" when the model bytes are identical.
124+
save_repo_id = f"{self.repo_id}-saved"
125+
with tempfile.TemporaryDirectory() as tmp_dir:
126+
pipeline.save_pretrained(tmp_dir, repo_id=save_repo_id, push_to_hub=True, token=TOKEN)
127+
128+
new_model = UNet2DConditionModel.from_pretrained(f"{USER}/{save_repo_id}", subfolder="unet")
129+
for p1, p2 in zip(unet.parameters(), new_model.parameters()):
130+
self.assertTrue(torch.equal(p1, p2))
131+
132+
# Reset repos
133+
delete_repo(token=TOKEN, repo_id=self.repo_id)
134+
delete_repo(save_repo_id, token=TOKEN)
135+
136+
def test_push_to_hub_in_organization(self):
137+
components = self.get_pipeline_components()
138+
pipeline = StableDiffusionPipeline(**components)
139+
pipeline.push_to_hub(self.org_repo_id, token=TOKEN)
140+
141+
new_model = UNet2DConditionModel.from_pretrained(self.org_repo_id, subfolder="unet")
142+
unet = components["unet"]
143+
for p1, p2 in zip(unet.parameters(), new_model.parameters()):
144+
self.assertTrue(torch.equal(p1, p2))
145+
146+
# Push to hub via save_pretrained to a separate repo. Reusing `self.org_repo_id` after
147+
# deleting it makes the staging server's LFS GC reject the next commit with
148+
# "LFS pointer pointed to a file that does not exist" when the model bytes are identical.
149+
save_org_repo_id = f"{self.org_repo_id}-saved"
150+
with tempfile.TemporaryDirectory() as tmp_dir:
151+
pipeline.save_pretrained(tmp_dir, push_to_hub=True, token=TOKEN, repo_id=save_org_repo_id)
152+
153+
new_model = UNet2DConditionModel.from_pretrained(save_org_repo_id, subfolder="unet")
154+
for p1, p2 in zip(unet.parameters(), new_model.parameters()):
155+
self.assertTrue(torch.equal(p1, p2))
156+
157+
# Reset repos
158+
delete_repo(token=TOKEN, repo_id=self.org_repo_id)
159+
delete_repo(save_org_repo_id, token=TOKEN)
160+
161+
@unittest.skipIf(
162+
not is_jinja_available(),
163+
reason="Model card tests cannot be performed without Jinja installed.",
164+
)
165+
def test_push_to_hub_library_name(self):
166+
components = self.get_pipeline_components()
167+
pipeline = StableDiffusionPipeline(**components)
168+
# Use a method-unique repo to avoid recycling a name that `test_push_to_hub` just deleted,
169+
# which the staging server rejects with an LFS pointer error.
170+
repo_id = f"test-pipeline-library-name-{uuid.uuid4()}"
171+
pipeline.push_to_hub(repo_id, token=TOKEN)
172+
173+
model_card = ModelCard.load(f"{USER}/{repo_id}", token=TOKEN).data
174+
assert model_card.library_name == "diffusers"
175+
176+
# Reset repo
177+
delete_repo(repo_id, token=TOKEN)

0 commit comments

Comments
 (0)