Repository navigation
stable_diffusion_3 model/pipeline review #13611
Description
stable_diffusion_3 model/pipeline review
Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423
Review performed against the repository review rules.
Issue 1: SD3 inpaint decode drops the VAE shift
Affected code:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3_inpaint.py
Lines 1364 to 1367 in 0f1abc4
| if not output_type == "latent": | |
| image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False, generator=generator)[ | |
| 0 | |
| ] |
Problem:
StableDiffusion3InpaintPipeline encodes VAE latents with (latents - shift_factor) * scaling_factor, but decodes with only latents / scaling_factor. The missing + self.vae.config.shift_factor makes inpaint decoding inconsistent with the other SD3 pipelines and with its own encode path. Duplicate search found no matching issue/PR.
Impact:
User-visible inpaint outputs are decoded from the wrong latent distribution whenever the VAE has a nonzero shift_factor, which SD3 VAEs do.
Reproduction:
import torch
from diffusers import AutoencoderKL
vae = AutoencoderKL(
sample_size=8, in_channels=3, out_channels=3, block_out_channels=(4,),
layers_per_block=1, latent_channels=16, norm_num_groups=1,
use_quant_conv=False, use_post_quant_conv=False,
shift_factor=0.25, scaling_factor=2.0,
)
latents = torch.randn(1, 16, 8, 8)
current = vae.decode(latents / vae.config.scaling_factor, return_dict=False)[0]
expected = vae.decode((latents / vae.config.scaling_factor) + vae.config.shift_factor, return_dict=False)[0]
print((current - expected).abs().max())Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py
Lines 1132 to 1136 in 0f1abc4
| else: | |
| latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor | |
| image = self.vae.decode(latents, return_dict=False)[0] | |
| image = self.image_processor.postprocess(image, output_type=output_type) |
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3_img2img.py
Lines 1146 to 1150 in 0f1abc4
| else: | |
| latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor | |
| image = self.vae.decode(latents, return_dict=False)[0] | |
| image = self.image_processor.postprocess(image, output_type=output_type) |
Suggested fix:
latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor
image = self.vae.decode(latents, return_dict=False, generator=generator)[0]Issue 2: SD3 ControlNet pipelines cannot use dynamic-shifting schedulers
Affected code:
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet.py
Lines 1101 to 1108 in 0f1abc4
| # 4. Prepare timesteps | |
| if XLA_AVAILABLE: | |
| timestep_device = "cpu" | |
| else: | |
| timestep_device = device | |
| timesteps, num_inference_steps = retrieve_timesteps( | |
| self.scheduler, num_inference_steps, timestep_device, sigmas=sigmas | |
| ) |
Lines 1272 to 1277 in 0f1abc4
| # 4. Prepare timesteps | |
| if XLA_AVAILABLE: | |
| timestep_device = "cpu" | |
| else: | |
| timestep_device = device | |
| timesteps, num_inference_steps = retrieve_timesteps( |
Problem:
The base SD3 pipelines compute and pass mu when FlowMatchEulerDiscreteScheduler.config.use_dynamic_shifting=True. The ControlNet SD3 pipelines call retrieve_timesteps() without mu handling and expose no mu argument. Duplicate search found no matching issue/PR.
Impact:
SD3.5-style scheduler configs with dynamic shifting fail before inference, so ControlNet is inconsistent with the rest of the SD3 family.
Reproduction:
from diffusers import FlowMatchEulerDiscreteScheduler
from diffusers.pipelines.controlnet_sd3.pipeline_stable_diffusion_3_controlnet import retrieve_timesteps
scheduler = FlowMatchEulerDiscreteScheduler(use_dynamic_shifting=True)
retrieve_timesteps(scheduler, num_inference_steps=2, device="cpu")Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py
Lines 1013 to 1038 in 0f1abc4
| if self.scheduler.config.get("use_dynamic_shifting", None) and mu is None: | |
| _, _, height, width = latents.shape | |
| image_seq_len = (height // self.transformer.config.patch_size) * ( | |
| width // self.transformer.config.patch_size | |
| ) | |
| mu = calculate_shift( | |
| image_seq_len, | |
| self.scheduler.config.get("base_image_seq_len", 256), | |
| self.scheduler.config.get("max_image_seq_len", 4096), | |
| self.scheduler.config.get("base_shift", 0.5), | |
| self.scheduler.config.get("max_shift", 1.16), | |
| ) | |
| scheduler_kwargs["mu"] = mu | |
| elif mu is not None: | |
| scheduler_kwargs["mu"] = mu | |
| if XLA_AVAILABLE: | |
| timestep_device = "cpu" | |
| else: | |
| timestep_device = device | |
| timesteps, num_inference_steps = retrieve_timesteps( | |
| self.scheduler, | |
| num_inference_steps, | |
| timestep_device, | |
| sigmas=sigmas, | |
| **scheduler_kwargs, | |
| ) |
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3_inpaint.py
Lines 1156 to 1177 in 0f1abc4
| if self.scheduler.config.get("use_dynamic_shifting", None) and mu is None: | |
| image_seq_len = (int(height) // self.vae_scale_factor // self.transformer.config.patch_size) * ( | |
| int(width) // self.vae_scale_factor // self.transformer.config.patch_size | |
| ) | |
| mu = calculate_shift( | |
| image_seq_len, | |
| self.scheduler.config.get("base_image_seq_len", 256), | |
| self.scheduler.config.get("max_image_seq_len", 4096), | |
| self.scheduler.config.get("base_shift", 0.5), | |
| self.scheduler.config.get("max_shift", 1.16), | |
| ) | |
| scheduler_kwargs["mu"] = mu | |
| elif mu is not None: | |
| scheduler_kwargs["mu"] = mu | |
| if XLA_AVAILABLE: | |
| timestep_device = "cpu" | |
| else: | |
| timestep_device = device | |
| timesteps, num_inference_steps = retrieve_timesteps( | |
| self.scheduler, num_inference_steps, timestep_device, sigmas=sigmas, **scheduler_kwargs | |
| ) |
Suggested fix:
Add the same mu argument, calculate_shift() logic, and scheduler_kwargs["mu"] handling used by the base SD3 pipelines before calling retrieve_timesteps().
Issue 3: Duplicate: controlnet_pooled_projections tensor path is broken
Affected code:
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet.py
Lines 1134 to 1138 in 0f1abc4
| if controlnet_config.force_zeros_for_pooled_projection: | |
| # instantx sd3 controlnet used zero pooled projection | |
| controlnet_pooled_projections = torch.zeros_like(pooled_prompt_embeds) | |
| else: | |
| controlnet_pooled_projections = controlnet_pooled_projections or pooled_prompt_embeds |
Lines 1267 to 1270 in 0f1abc4
| if controlnet_pooled_projections is None: | |
| controlnet_pooled_projections = torch.zeros_like(pooled_prompt_embeds) | |
| else: | |
| controlnet_pooled_projections = controlnet_pooled_projections or pooled_prompt_embeds |
Problem:
This is already reported in open issue #9686. When a user passes a tensor for controlnet_pooled_projections, the code evaluates it with Python or, which raises RuntimeError: Boolean value of Tensor with more than one value is ambiguous.
Impact:
The public controlnet_pooled_projections argument cannot be used reliably, and SD3 ControlNet inference/training validation can diverge from the intended pooled-projection conditioning path.
Reproduction:
import torch
controlnet_pooled_projections = torch.ones(1, 8)
pooled_prompt_embeds = torch.zeros(1, 8)
controlnet_pooled_projections = controlnet_pooled_projections or pooled_prompt_embedsRelevant precedent:
Existing duplicate: #9686
Suggested fix:
if controlnet_config.force_zeros_for_pooled_projection:
controlnet_pooled_projections = torch.zeros_like(pooled_prompt_embeds)
elif controlnet_pooled_projections is None:
controlnet_pooled_projections = pooled_prompt_embedsIssue 4: SD3ControlNetModel.from_transformer() mutates the source transformer config
Affected code:
diffusers/src/diffusers/models/controlnets/controlnet_sd3.py
Lines 253 to 260 in 0f1abc4
| @classmethod | |
| def from_transformer( | |
| cls, transformer, num_layers=12, num_extra_conditioning_channels=1, load_weights_from_transformer=True | |
| ): | |
| config = transformer.config | |
| config["num_layers"] = num_layers or config.num_layers | |
| config["extra_conditioning_channels"] = num_extra_conditioning_channels | |
| controlnet = cls.from_config(config) |
Problem:
config = transformer.config aliases the transformer's live config, then writes ControlNet-specific values into it. Duplicate search found no matching issue/PR.
Impact:
Calling from_transformer() silently changes transformer.config.num_layers and adds extra_conditioning_channels, which can corrupt later serialization, logging, or pipeline construction using the original transformer.
Reproduction:
from diffusers import SD3ControlNetModel, SD3Transformer2DModel
transformer = SD3Transformer2DModel(
sample_size=4, patch_size=1, in_channels=4, out_channels=4, num_layers=3,
attention_head_dim=4, num_attention_heads=2, caption_projection_dim=8,
joint_attention_dim=8, pooled_projection_dim=8,
)
print(dict(transformer.config).get("num_layers"))
SD3ControlNetModel.from_transformer(transformer, num_layers=1, num_extra_conditioning_channels=2, load_weights_from_transformer=False)
print(dict(transformer.config).get("num_layers"), dict(transformer.config).get("extra_conditioning_channels"))Relevant precedent:
diffusers/src/diffusers/models/controlnets/controlnet_flux.py
Lines 135 to 141 in 0f1abc4
| config = dict(transformer.config) | |
| config["num_layers"] = num_layers | |
| config["num_single_layers"] = num_single_layers | |
| config["attention_head_dim"] = attention_head_dim | |
| config["num_attention_heads"] = num_attention_heads | |
| controlnet = cls.from_config(config) |
diffusers/src/diffusers/models/controlnets/controlnet_qwenimage.py
Lines 113 to 119 in 0f1abc4
| config = dict(transformer.config) | |
| config["num_layers"] = num_layers | |
| config["attention_head_dim"] = attention_head_dim | |
| config["num_attention_heads"] = num_attention_heads | |
| config["extra_condition_channels"] = extra_condition_channels | |
| controlnet = cls.from_config(config) |
Suggested fix:
config = dict(transformer.config)
config["num_layers"] = num_layers or transformer.config.num_layers
config["extra_conditioning_channels"] = num_extra_conditioning_channels
controlnet = cls.from_config(config)Issue 5: ControlNet inpaint rejects documented IP-Adapter image embeds
Affected code:
Lines 780 to 787 in 0f1abc4
| if ip_adapter_image_embeds is not None: | |
| if not isinstance(ip_adapter_image_embeds, list): | |
| raise ValueError( | |
| f"`ip_adapter_image_embeds` has to be of type `list` but is {type(ip_adapter_image_embeds)}" | |
| ) | |
| elif ip_adapter_image_embeds[0].ndim not in [3, 4]: | |
| raise ValueError( | |
| f"`ip_adapter_image_embeds` has to be a list of 3D or 4D tensors but is {ip_adapter_image_embeds[0].ndim}D" |
Lines 928 to 969 in 0f1abc4
| # Copied from diffusers.pipelines.stable_diffusion_3.pipeline_stable_diffusion_3.StableDiffusion3Pipeline.prepare_ip_adapter_image_embeds | |
| def prepare_ip_adapter_image_embeds( | |
| self, | |
| ip_adapter_image: PipelineImageInput | None = None, | |
| ip_adapter_image_embeds: torch.Tensor | None = None, | |
| device: torch.device | None = None, | |
| num_images_per_prompt: int = 1, | |
| do_classifier_free_guidance: bool = True, | |
| ) -> torch.Tensor: | |
| """Prepares image embeddings for use in the IP-Adapter. | |
| Either `ip_adapter_image` or `ip_adapter_image_embeds` must be passed. | |
| Args: | |
| ip_adapter_image (`PipelineImageInput`, *optional*): | |
| The input image to extract features from for IP-Adapter. | |
| ip_adapter_image_embeds (`torch.Tensor`, *optional*): | |
| Precomputed image embeddings. | |
| device: (`torch.device`, *optional*): | |
| Torch device. | |
| num_images_per_prompt (`int`, defaults to 1): | |
| Number of images that should be generated per prompt. | |
| do_classifier_free_guidance (`bool`, defaults to True): | |
| Whether to use classifier free guidance or not. | |
| """ | |
| device = device or self._execution_device | |
| if ip_adapter_image_embeds is not None: | |
| if do_classifier_free_guidance: | |
| single_negative_image_embeds, single_image_embeds = ip_adapter_image_embeds.chunk(2) | |
| else: | |
| single_image_embeds = ip_adapter_image_embeds | |
| elif ip_adapter_image is not None: | |
| single_image_embeds = self.encode_image(ip_adapter_image, device) | |
| if do_classifier_free_guidance: | |
| single_negative_image_embeds = torch.zeros_like(single_image_embeds) | |
| else: | |
| raise ValueError("Neither `ip_adapter_image_embeds` or `ip_adapter_image_embeds` were provided.") | |
| image_embeds = torch.cat([single_image_embeds] * num_images_per_prompt, dim=0) | |
| if do_classifier_free_guidance: |
Lines 1111 to 1114 in 0f1abc4
| ip_adapter_image_embeds (`torch.Tensor`, *optional*): | |
| Pre-generated image embeddings for IP-Adapter. Should be a tensor of shape `(batch_size, num_images, | |
| emb_dim)`. It should contain the negative image embedding if `do_classifier_free_guidance` is set to | |
| `True`. If not provided, embeddings are computed from the `ip_adapter_image` input argument. |
Problem:
The docstring and prepare_ip_adapter_image_embeds() path expect ip_adapter_image_embeds to be a tensor, but check_inputs() rejects tensors and requires a list. Duplicate search found no matching issue/PR.
Impact:
Users cannot pass precomputed IP-Adapter image embeddings to StableDiffusion3ControlNetInpaintingPipeline even though the public signature documents that path.
Reproduction:
import torch
from diffusers import StableDiffusion3ControlNetInpaintingPipeline
pipe = object.__new__(StableDiffusion3ControlNetInpaintingPipeline)
pipe.vae_scale_factor = 1
pipe.patch_size = 1
pipe._callback_tensor_inputs = ["latents"]
pipe.controlnet = object()
pipe.check_inputs(
height=8, width=8, image=torch.zeros(1, 3, 8, 8),
prompt=None, prompt_2=None, prompt_3=None,
prompt_embeds=torch.zeros(1, 2, 8),
pooled_prompt_embeds=torch.zeros(1, 8),
ip_adapter_image_embeds=torch.zeros(1, 2, 8),
control_guidance_start=[0.0], control_guidance_end=[1.0],
)Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py
Lines 742 to 760 in 0f1abc4
| if ip_adapter_image_embeds is not None: | |
| if do_classifier_free_guidance: | |
| single_negative_image_embeds, single_image_embeds = ip_adapter_image_embeds.chunk(2) | |
| else: | |
| single_image_embeds = ip_adapter_image_embeds | |
| elif ip_adapter_image is not None: | |
| single_image_embeds = self.encode_image(ip_adapter_image, device) | |
| if do_classifier_free_guidance: | |
| single_negative_image_embeds = torch.zeros_like(single_image_embeds) | |
| else: | |
| raise ValueError("Neither `ip_adapter_image_embeds` or `ip_adapter_image_embeds` were provided.") | |
| image_embeds = torch.cat([single_image_embeds] * num_images_per_prompt, dim=0) | |
| if do_classifier_free_guidance: | |
| negative_image_embeds = torch.cat([single_negative_image_embeds] * num_images_per_prompt, dim=0) | |
| image_embeds = torch.cat([negative_image_embeds, image_embeds], dim=0) | |
| return image_embeds.to(device=device) |
Suggested fix:
if ip_adapter_image_embeds is not None and ip_adapter_image_embeds.ndim not in [3, 4]:
raise ValueError(
f"`ip_adapter_image_embeds` has to be a 3D or 4D tensor but is {ip_adapter_image_embeds.ndim}D"
)Issue 6: Slow tests are missing for SD3 inpaint and SD3 ControlNet inpaint
Affected code:
diffusers/tests/pipelines/stable_diffusion_3/test_pipeline_stable_diffusion_3_inpaint.py
Lines 38 to 163 in 0f1abc4
| class StableDiffusion3InpaintPipelineFastTests(PipelineLatentTesterMixin, unittest.TestCase, PipelineTesterMixin): | |
| pipeline_class = StableDiffusion3InpaintPipeline | |
| params = TEXT_GUIDED_IMAGE_INPAINTING_PARAMS | |
| required_optional_params = PipelineTesterMixin.required_optional_params | |
| batch_params = TEXT_GUIDED_IMAGE_INPAINTING_BATCH_PARAMS | |
| image_params = frozenset( | |
| [] | |
| ) # TO-DO: update image_params once pipeline is refactored with VaeImageProcessor.preprocess | |
| image_latents_params = frozenset([]) | |
| callback_cfg_params = TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"mask", "masked_image_latents"}) | |
| def get_dummy_components(self): | |
| torch.manual_seed(0) | |
| transformer = SD3Transformer2DModel( | |
| sample_size=32, | |
| patch_size=1, | |
| in_channels=16, | |
| num_layers=1, | |
| attention_head_dim=8, | |
| num_attention_heads=4, | |
| joint_attention_dim=32, | |
| caption_projection_dim=32, | |
| pooled_projection_dim=64, | |
| out_channels=16, | |
| ) | |
| clip_text_encoder_config = CLIPTextConfig( | |
| bos_token_id=0, | |
| eos_token_id=2, | |
| hidden_size=32, | |
| intermediate_size=37, | |
| layer_norm_eps=1e-05, | |
| num_attention_heads=4, | |
| num_hidden_layers=5, | |
| pad_token_id=1, | |
| vocab_size=1000, | |
| hidden_act="gelu", | |
| projection_dim=32, | |
| ) | |
| torch.manual_seed(0) | |
| text_encoder = CLIPTextModelWithProjection(clip_text_encoder_config) | |
| torch.manual_seed(0) | |
| text_encoder_2 = CLIPTextModelWithProjection(clip_text_encoder_config) | |
| torch.manual_seed(0) | |
| config = AutoConfig.from_pretrained("hf-internal-testing/tiny-random-t5") | |
| text_encoder_3 = T5EncoderModel(config) | |
| tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") | |
| tokenizer_2 = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") | |
| tokenizer_3 = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5") | |
| torch.manual_seed(0) | |
| vae = AutoencoderKL( | |
| sample_size=32, | |
| in_channels=3, | |
| out_channels=3, | |
| block_out_channels=(4,), | |
| layers_per_block=1, | |
| latent_channels=16, | |
| norm_num_groups=1, | |
| use_quant_conv=False, | |
| use_post_quant_conv=False, | |
| shift_factor=0.0609, | |
| scaling_factor=1.5035, | |
| ) | |
| scheduler = FlowMatchEulerDiscreteScheduler() | |
| return { | |
| "scheduler": scheduler, | |
| "text_encoder": text_encoder, | |
| "text_encoder_2": text_encoder_2, | |
| "text_encoder_3": text_encoder_3, | |
| "tokenizer": tokenizer, | |
| "tokenizer_2": tokenizer_2, | |
| "tokenizer_3": tokenizer_3, | |
| "transformer": transformer, | |
| "vae": vae, | |
| "image_encoder": None, | |
| "feature_extractor": None, | |
| } | |
| def get_dummy_inputs(self, device, seed=0): | |
| image = floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(device) | |
| mask_image = torch.ones((1, 1, 32, 32)).to(device) | |
| if str(device).startswith("mps"): | |
| generator = torch.manual_seed(seed) | |
| else: | |
| generator = torch.Generator(device="cpu").manual_seed(seed) | |
| inputs = { | |
| "prompt": "A painting of a squirrel eating a burger", | |
| "image": image, | |
| "mask_image": mask_image, | |
| "height": 32, | |
| "width": 32, | |
| "generator": generator, | |
| "num_inference_steps": 2, | |
| "guidance_scale": 5.0, | |
| "output_type": "np", | |
| "strength": 0.8, | |
| } | |
| return inputs | |
| def test_inference(self): | |
| components = self.get_dummy_components() | |
| pipe = self.pipeline_class(**components) | |
| inputs = self.get_dummy_inputs(torch_device) | |
| image = pipe(**inputs).images[0] | |
| generated_slice = image.flatten() | |
| generated_slice = np.concatenate([generated_slice[:8], generated_slice[-8:]]) | |
| # fmt: off | |
| expected_slice = np.array([0.5035, 0.6661, 0.5859, 0.413, 0.4224, 0.4234, 0.7181, 0.5062, 0.5183, 0.6877, 0.5074, 0.585, 0.6111, 0.5422, 0.5306, 0.5891]) | |
| # fmt: on | |
| self.assertTrue( | |
| np.allclose(generated_slice, expected_slice, atol=1e-3), "Output does not match expected slice." | |
| ) | |
| @unittest.skip("Skip for now.") | |
| def test_multi_vae(self): | |
| pass |
diffusers/tests/pipelines/controlnet_sd3/test_controlnet_inpaint_sd3.py
Lines 45 to 208 in 0f1abc4
| class StableDiffusion3ControlInpaintNetPipelineFastTests(unittest.TestCase, PipelineTesterMixin): | |
| pipeline_class = StableDiffusion3ControlNetInpaintingPipeline | |
| params = frozenset( | |
| [ | |
| "prompt", | |
| "height", | |
| "width", | |
| "guidance_scale", | |
| "negative_prompt", | |
| "prompt_embeds", | |
| "negative_prompt_embeds", | |
| ] | |
| ) | |
| batch_params = frozenset(["prompt", "negative_prompt"]) | |
| def get_dummy_components(self): | |
| torch.manual_seed(0) | |
| transformer = SD3Transformer2DModel( | |
| sample_size=32, | |
| patch_size=1, | |
| in_channels=8, | |
| num_layers=4, | |
| attention_head_dim=8, | |
| num_attention_heads=4, | |
| joint_attention_dim=32, | |
| caption_projection_dim=32, | |
| pooled_projection_dim=64, | |
| out_channels=8, | |
| ) | |
| torch.manual_seed(0) | |
| controlnet = SD3ControlNetModel( | |
| sample_size=32, | |
| patch_size=1, | |
| in_channels=8, | |
| num_layers=1, | |
| attention_head_dim=8, | |
| num_attention_heads=4, | |
| joint_attention_dim=32, | |
| caption_projection_dim=32, | |
| pooled_projection_dim=64, | |
| out_channels=8, | |
| extra_conditioning_channels=1, | |
| ) | |
| clip_text_encoder_config = CLIPTextConfig( | |
| bos_token_id=0, | |
| eos_token_id=2, | |
| hidden_size=32, | |
| intermediate_size=37, | |
| layer_norm_eps=1e-05, | |
| num_attention_heads=4, | |
| num_hidden_layers=5, | |
| pad_token_id=1, | |
| vocab_size=1000, | |
| hidden_act="gelu", | |
| projection_dim=32, | |
| ) | |
| torch.manual_seed(0) | |
| text_encoder = CLIPTextModelWithProjection(clip_text_encoder_config) | |
| torch.manual_seed(0) | |
| text_encoder_2 = CLIPTextModelWithProjection(clip_text_encoder_config) | |
| torch.manual_seed(0) | |
| config = AutoConfig.from_pretrained("hf-internal-testing/tiny-random-t5") | |
| text_encoder_3 = T5EncoderModel(config) | |
| tokenizer = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") | |
| tokenizer_2 = CLIPTokenizer.from_pretrained("hf-internal-testing/tiny-random-clip") | |
| tokenizer_3 = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-t5") | |
| torch.manual_seed(0) | |
| vae = AutoencoderKL( | |
| sample_size=32, | |
| in_channels=3, | |
| out_channels=3, | |
| block_out_channels=(4,), | |
| layers_per_block=1, | |
| latent_channels=8, | |
| norm_num_groups=1, | |
| use_quant_conv=False, | |
| use_post_quant_conv=False, | |
| shift_factor=0.0609, | |
| scaling_factor=1.5035, | |
| ) | |
| scheduler = FlowMatchEulerDiscreteScheduler() | |
| return { | |
| "scheduler": scheduler, | |
| "text_encoder": text_encoder, | |
| "text_encoder_2": text_encoder_2, | |
| "text_encoder_3": text_encoder_3, | |
| "tokenizer": tokenizer, | |
| "tokenizer_2": tokenizer_2, | |
| "tokenizer_3": tokenizer_3, | |
| "transformer": transformer, | |
| "vae": vae, | |
| "controlnet": controlnet, | |
| "image_encoder": None, | |
| "feature_extractor": None, | |
| } | |
| def get_dummy_inputs(self, device, seed=0): | |
| if str(device).startswith("mps"): | |
| generator = torch.manual_seed(seed) | |
| else: | |
| generator = torch.Generator(device="cpu").manual_seed(seed) | |
| control_image = randn_tensor( | |
| (1, 3, 32, 32), | |
| generator=generator, | |
| device=torch.device(device), | |
| dtype=torch.float16, | |
| ) | |
| control_mask = randn_tensor( | |
| (1, 1, 32, 32), | |
| generator=generator, | |
| device=torch.device(device), | |
| dtype=torch.float16, | |
| ) | |
| controlnet_conditioning_scale = 0.95 | |
| inputs = { | |
| "prompt": "A painting of a squirrel eating a burger", | |
| "generator": generator, | |
| "num_inference_steps": 2, | |
| "guidance_scale": 7.0, | |
| "output_type": "np", | |
| "control_image": control_image, | |
| "control_mask": control_mask, | |
| "controlnet_conditioning_scale": controlnet_conditioning_scale, | |
| } | |
| return inputs | |
| def test_controlnet_inpaint_sd3(self): | |
| components = self.get_dummy_components() | |
| sd_pipe = StableDiffusion3ControlNetInpaintingPipeline(**components) | |
| sd_pipe = sd_pipe.to(torch_device, dtype=torch.float16) | |
| sd_pipe.set_progress_bar_config(disable=None) | |
| inputs = self.get_dummy_inputs(torch_device) | |
| output = sd_pipe(**inputs) | |
| image = output.images | |
| image_slice = image[0, -3:, -3:, -1] | |
| assert image.shape == (1, 32, 32, 3) | |
| expected_slice = np.array( | |
| [0.51708984, 0.7421875, 0.4580078, 0.6435547, 0.65625, 0.43603516, 0.5151367, 0.65722656, 0.60839844] | |
| ) | |
| assert np.abs(image_slice.flatten() - expected_slice).max() < 1e-2, ( | |
| f"Expected: {expected_slice}, got: {image_slice.flatten()}" | |
| ) | |
| @unittest.skip("xFormersAttnProcessor does not work with SD3 Joint Attention") | |
| def test_xformers_attention_forwardGenerator_pass(self): | |
| pass |
Problem:
Both files only define fast tests. The target family has slow coverage for SD3 text-to-image, SD3 img2img, and SD3 ControlNet, but not for the two inpaint variants. Duplicate search found no matching issue/PR.
Impact:
Real checkpoint behavior, offload behavior, VAE shift handling, and image/mask preprocessing are not covered for the inpaint variants. This gap would have allowed Issue 1 to remain invisible in CI.
Reproduction:
from pathlib import Path
for path in [
Path("tests/pipelines/stable_diffusion_3/test_pipeline_stable_diffusion_3_inpaint.py"),
Path("tests/pipelines/controlnet_sd3/test_controlnet_inpaint_sd3.py"),
]:
text = path.read_text()
print(path, "@slow" in text, "SlowTests" in text)Relevant precedent:
diffusers/tests/pipelines/stable_diffusion_3/test_pipeline_stable_diffusion_3.py
Lines 227 to 235 in 0f1abc4
| @slow | |
| @require_big_accelerator | |
| class StableDiffusion3PipelineSlowTests(unittest.TestCase): | |
| pipeline_class = StableDiffusion3Pipeline | |
| repo_id = "stabilityai/stable-diffusion-3-medium-diffusers" | |
| def setUp(self): | |
| super().setUp() | |
| gc.collect() |
diffusers/tests/pipelines/stable_diffusion_3/test_pipeline_stable_diffusion_3_img2img.py
Lines 162 to 166 in 0f1abc4
| @slow | |
| @require_big_accelerator | |
| class StableDiffusion3Img2ImgPipelineSlowTests(unittest.TestCase): | |
| pipeline_class = StableDiffusion3Img2ImgPipeline | |
| repo_id = "stabilityai/stable-diffusion-3-medium-diffusers" |
diffusers/tests/pipelines/controlnet_sd3/test_controlnet_sd3.py
Lines 228 to 245 in 0f1abc4
| @slow | |
| @require_big_accelerator | |
| class StableDiffusion3ControlNetPipelineSlowTests(unittest.TestCase): | |
| pipeline_class = StableDiffusion3ControlNetPipeline | |
| def setUp(self): | |
| super().setUp() | |
| gc.collect() | |
| backend_empty_cache(torch_device) | |
| def tearDown(self): | |
| super().tearDown() | |
| gc.collect() | |
| backend_empty_cache(torch_device) | |
| def test_canny(self): | |
| controlnet = SD3ControlNetModel.from_pretrained("InstantX/SD3-Controlnet-Canny", torch_dtype=torch.float16) | |
| pipe = StableDiffusion3ControlNetPipeline.from_pretrained( |
Suggested fix:
Add @slow / @require_big_accelerator classes for StableDiffusion3InpaintPipeline and StableDiffusion3ControlNetInpaintingPipeline using real SD3-family checkpoints, CPU/GPU offload, and deterministic output-slice assertions.
Activity