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:
| ifnotoutput_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:
importtorchfromdiffusersimportAutoencoderKLvae=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:
| 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) |
| 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_factorimage=self.vae.decode(latents, return_dict=False, generator=generator)[0]
Issue 2: SD3 ControlNet pipelines cannot use dynamic-shifting schedulers
Affected code:
| # 4. Prepare timesteps |
| ifXLA_AVAILABLE: |
| timestep_device="cpu" |
| else: |
| timestep_device=device |
| timesteps, num_inference_steps=retrieve_timesteps( |
| self.scheduler, num_inference_steps, timestep_device, sigmas=sigmas |
| ) |
| # 4. Prepare timesteps |
| ifXLA_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:
fromdiffusersimportFlowMatchEulerDiscreteSchedulerfromdiffusers.pipelines.controlnet_sd3.pipeline_stable_diffusion_3_controlnetimportretrieve_timestepsscheduler=FlowMatchEulerDiscreteScheduler(use_dynamic_shifting=True)
retrieve_timesteps(scheduler, num_inference_steps=2, device="cpu")
Relevant precedent:
| ifself.scheduler.config.get("use_dynamic_shifting", None) andmuisNone: |
| _, _, 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 |
| elifmuisnotNone: |
| scheduler_kwargs["mu"] =mu |
| ifXLA_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, |
| ) |
| ifself.scheduler.config.get("use_dynamic_shifting", None) andmuisNone: |
| 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 |
| elifmuisnotNone: |
| scheduler_kwargs["mu"] =mu |
| |
| ifXLA_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:
| ifcontrolnet_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_projectionsorpooled_prompt_embeds |
| ifcontrolnet_pooled_projectionsisNone: |
| controlnet_pooled_projections=torch.zeros_like(pooled_prompt_embeds) |
| else: |
| controlnet_pooled_projections=controlnet_pooled_projectionsorpooled_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:
importtorchcontrolnet_pooled_projections=torch.ones(1, 8)
pooled_prompt_embeds=torch.zeros(1, 8)
controlnet_pooled_projections=controlnet_pooled_projectionsorpooled_prompt_embeds
Relevant precedent:
Existing duplicate: #9686
Suggested fix:
ifcontrolnet_config.force_zeros_for_pooled_projection:
controlnet_pooled_projections=torch.zeros_like(pooled_prompt_embeds)
elifcontrolnet_pooled_projectionsisNone:
controlnet_pooled_projections=pooled_prompt_embeds
Issue 4: SD3ControlNetModel.from_transformer() mutates the source transformer config
Affected code:
| @classmethod |
| deffrom_transformer( |
| cls, transformer, num_layers=12, num_extra_conditioning_channels=1, load_weights_from_transformer=True |
| ): |
| config=transformer.config |
| config["num_layers"] =num_layersorconfig.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:
fromdiffusersimportSD3ControlNetModel, SD3Transformer2DModeltransformer=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:
| 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) |
| 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_layersortransformer.config.num_layersconfig["extra_conditioning_channels"] =num_extra_conditioning_channelscontrolnet=cls.from_config(config)
Issue 5: ControlNet inpaint rejects documented IP-Adapter image embeds
Affected code:
| ifip_adapter_image_embedsisnotNone: |
| ifnotisinstance(ip_adapter_image_embeds, list): |
| raiseValueError( |
| f"`ip_adapter_image_embeds` has to be of type `list` but is {type(ip_adapter_image_embeds)}" |
| ) |
| elifip_adapter_image_embeds[0].ndimnotin [3, 4]: |
| raiseValueError( |
| f"`ip_adapter_image_embeds` has to be a list of 3D or 4D tensors but is {ip_adapter_image_embeds[0].ndim}D" |
| # Copied from diffusers.pipelines.stable_diffusion_3.pipeline_stable_diffusion_3.StableDiffusion3Pipeline.prepare_ip_adapter_image_embeds |
| defprepare_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=deviceorself._execution_device |
| |
| ifip_adapter_image_embedsisnotNone: |
| ifdo_classifier_free_guidance: |
| single_negative_image_embeds, single_image_embeds=ip_adapter_image_embeds.chunk(2) |
| else: |
| single_image_embeds=ip_adapter_image_embeds |
| elifip_adapter_imageisnotNone: |
| single_image_embeds=self.encode_image(ip_adapter_image, device) |
| ifdo_classifier_free_guidance: |
| single_negative_image_embeds=torch.zeros_like(single_image_embeds) |
| else: |
| raiseValueError("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) |
| |
| ifdo_classifier_free_guidance: |
| 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:
importtorchfromdiffusersimportStableDiffusion3ControlNetInpaintingPipelinepipe=object.__new__(StableDiffusion3ControlNetInpaintingPipeline)
pipe.vae_scale_factor=1pipe.patch_size=1pipe._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:
| ifip_adapter_image_embedsisnotNone: |
| ifdo_classifier_free_guidance: |
| single_negative_image_embeds, single_image_embeds=ip_adapter_image_embeds.chunk(2) |
| else: |
| single_image_embeds=ip_adapter_image_embeds |
| elifip_adapter_imageisnotNone: |
| single_image_embeds=self.encode_image(ip_adapter_image, device) |
| ifdo_classifier_free_guidance: |
| single_negative_image_embeds=torch.zeros_like(single_image_embeds) |
| else: |
| raiseValueError("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) |
| |
| ifdo_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) |
| |
| returnimage_embeds.to(device=device) |
Suggested fix:
ifip_adapter_image_embedsisnotNoneandip_adapter_image_embeds.ndimnotin [3, 4]:
raiseValueError(
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:
| classStableDiffusion3InpaintPipelineFastTests(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"}) |
| |
| defget_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, |
| } |
| |
| defget_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) |
| ifstr(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, |
| } |
| returninputs |
| |
| deftest_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.") |
| deftest_multi_vae(self): |
| pass |
| classStableDiffusion3ControlInpaintNetPipelineFastTests(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"]) |
| |
| defget_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, |
| } |
| |
| defget_dummy_inputs(self, device, seed=0): |
| ifstr(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, |
| } |
| |
| returninputs |
| |
| deftest_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] |
| |
| assertimage.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] |
| ) |
| |
| assertnp.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") |
| deftest_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:
frompathlibimportPathforpathin [
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"intext, "SlowTests"intext)Relevant precedent:
| @slow |
| @require_big_accelerator |
| classStableDiffusion3PipelineSlowTests(unittest.TestCase): |
| pipeline_class=StableDiffusion3Pipeline |
| repo_id="stabilityai/stable-diffusion-3-medium-diffusers" |
| |
| defsetUp(self): |
| super().setUp() |
| gc.collect() |
| @slow |
| @require_big_accelerator |
| classStableDiffusion3Img2ImgPipelineSlowTests(unittest.TestCase): |
| pipeline_class=StableDiffusion3Img2ImgPipeline |
| repo_id="stabilityai/stable-diffusion-3-medium-diffusers" |
| @slow |
| @require_big_accelerator |
| classStableDiffusion3ControlNetPipelineSlowTests(unittest.TestCase): |
| pipeline_class=StableDiffusion3ControlNetPipeline |
| |
| defsetUp(self): |
| super().setUp() |
| gc.collect() |
| backend_empty_cache(torch_device) |
| |
| deftearDown(self): |
| super().tearDown() |
| gc.collect() |
| backend_empty_cache(torch_device) |
| |
| deftest_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.
stable_diffusion_3model/pipeline reviewCommit tested:
0f1abc4ae8b0eb2a3b40e82a310507281144c423Review 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
Problem:
StableDiffusion3InpaintPipelineencodes VAE latents with(latents - shift_factor) * scaling_factor, but decodes with onlylatents / scaling_factor. The missing+ self.vae.config.shift_factormakes 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:
Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py
Lines 1132 to 1136 in 0f1abc4
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3_img2img.py
Lines 1146 to 1150 in 0f1abc4
Suggested fix:
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
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet_inpainting.py
Lines 1272 to 1277 in 0f1abc4
Problem:
The base SD3 pipelines compute and pass
muwhenFlowMatchEulerDiscreteScheduler.config.use_dynamic_shifting=True. The ControlNet SD3 pipelines callretrieve_timesteps()withoutmuhandling and expose nomuargument. 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:
Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py
Lines 1013 to 1038 in 0f1abc4
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3_inpaint.py
Lines 1156 to 1177 in 0f1abc4
Suggested fix:
Add the same
muargument,calculate_shift()logic, andscheduler_kwargs["mu"]handling used by the base SD3 pipelines before callingretrieve_timesteps().Issue 3: Duplicate:
controlnet_pooled_projectionstensor path is brokenAffected code:
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet.py
Lines 1134 to 1138 in 0f1abc4
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet_inpainting.py
Lines 1267 to 1270 in 0f1abc4
Problem:
This is already reported in open issue #9686. When a user passes a tensor for
controlnet_pooled_projections, the code evaluates it with Pythonor, which raisesRuntimeError: Boolean value of Tensor with more than one value is ambiguous.Impact:
The public
controlnet_pooled_projectionsargument cannot be used reliably, and SD3 ControlNet inference/training validation can diverge from the intended pooled-projection conditioning path.Reproduction:
Relevant precedent:
Existing duplicate: #9686
Suggested fix:
Issue 4:
SD3ControlNetModel.from_transformer()mutates the source transformer configAffected code:
diffusers/src/diffusers/models/controlnets/controlnet_sd3.py
Lines 253 to 260 in 0f1abc4
Problem:
config = transformer.configaliases the transformer's live config, then writes ControlNet-specific values into it. Duplicate search found no matching issue/PR.Impact:
Calling
from_transformer()silently changestransformer.config.num_layersand addsextra_conditioning_channels, which can corrupt later serialization, logging, or pipeline construction using the original transformer.Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/controlnets/controlnet_flux.py
Lines 135 to 141 in 0f1abc4
diffusers/src/diffusers/models/controlnets/controlnet_qwenimage.py
Lines 113 to 119 in 0f1abc4
Suggested fix:
Issue 5: ControlNet inpaint rejects documented IP-Adapter image embeds
Affected code:
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet_inpainting.py
Lines 780 to 787 in 0f1abc4
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet_inpainting.py
Lines 928 to 969 in 0f1abc4
diffusers/src/diffusers/pipelines/controlnet_sd3/pipeline_stable_diffusion_3_controlnet_inpainting.py
Lines 1111 to 1114 in 0f1abc4
Problem:
The docstring and
prepare_ip_adapter_image_embeds()path expectip_adapter_image_embedsto be a tensor, butcheck_inputs()rejects tensors and requires a list. Duplicate search found no matching issue/PR.Impact:
Users cannot pass precomputed IP-Adapter image embeddings to
StableDiffusion3ControlNetInpaintingPipelineeven though the public signature documents that path.Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion_3/pipeline_stable_diffusion_3.py
Lines 742 to 760 in 0f1abc4
Suggested fix:
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
diffusers/tests/pipelines/controlnet_sd3/test_controlnet_inpaint_sd3.py
Lines 45 to 208 in 0f1abc4
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:
Relevant precedent:
diffusers/tests/pipelines/stable_diffusion_3/test_pipeline_stable_diffusion_3.py
Lines 227 to 235 in 0f1abc4
diffusers/tests/pipelines/stable_diffusion_3/test_pipeline_stable_diffusion_3_img2img.py
Lines 162 to 166 in 0f1abc4
diffusers/tests/pipelines/controlnet_sd3/test_controlnet_sd3.py
Lines 228 to 245 in 0f1abc4
Suggested fix:
Add
@slow/@require_big_acceleratorclasses forStableDiffusion3InpaintPipelineandStableDiffusion3ControlNetInpaintingPipelineusing real SD3-family checkpoints, CPU/GPU offload, and deterministic output-slice assertions.