deepfloyd_if model/pipeline review
Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423
Review performed against the repository review rules. Note: .ai/review-rules.md references AGENTS.md, but that file is absent in this checkout; the remaining referenced rule files were applied.
Duplicate search status: searched GitHub Issues and PRs for deepfloyd_if, affected class/function names, NumPy image/mask failures, IFPipelineOutput import behavior, strength validation, and T5FilmDecoder coverage. I found no likely duplicates.
Issue 1: Unbatched NumPy image inputs are rejected as batch-size mismatches
Affected code:
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| # image |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| |
| # mask_image |
| |
| ifisinstance(mask_image, list): |
| check_image_type=mask_image[0] |
| else: |
| check_image_type=mask_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`mask_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(mask_image, list): |
| image_batch_size=len(mask_image) |
| elifisinstance(mask_image, torch.Tensor): |
| image_batch_size=mask_image.shape[0] |
| elifisinstance(mask_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(mask_image, np.ndarray): |
| image_batch_size=mask_image.shape[0] |
| else: |
| assertFalse |
| |
| ifimage_batch_size!=1andbatch_size!=image_batch_size: |
| raiseValueError( |
| f"mask_image batch size: {image_batch_size} must be `1` or the same as prompt batch size {batch_size}" |
| ) |
| # image |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| |
| # original_image |
| |
| ifisinstance(original_image, list): |
| check_image_type=original_image[0] |
| else: |
| check_image_type=original_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`original_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(original_image, list): |
| image_batch_size=len(original_image) |
| elifisinstance(original_image, torch.Tensor): |
| image_batch_size=original_image.shape[0] |
| elifisinstance(original_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(original_image, np.ndarray): |
| image_batch_size=original_image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError( |
| f"original_image batch size: {image_batch_size} must be same as prompt batch size {batch_size}" |
| ) |
| f" {negative_prompt_embeds.shape}." |
| ) |
| |
| # image |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| |
| # original_image |
| |
| ifisinstance(original_image, list): |
| check_image_type=original_image[0] |
| else: |
| check_image_type=original_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`original_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(original_image, list): |
| image_batch_size=len(original_image) |
| elifisinstance(original_image, torch.Tensor): |
| image_batch_size=original_image.shape[0] |
| elifisinstance(original_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(original_image, np.ndarray): |
| image_batch_size=original_image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError( |
| f"original_image batch size: {image_batch_size} must be same as prompt batch size {batch_size}" |
| ) |
| |
| # mask_image |
| |
| ifisinstance(mask_image, list): |
| check_image_type=mask_image[0] |
| else: |
| check_image_type=mask_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`mask_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(mask_image, list): |
| image_batch_size=len(mask_image) |
| elifisinstance(mask_image, torch.Tensor): |
| image_batch_size=mask_image.shape[0] |
| elifisinstance(mask_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(mask_image, np.ndarray): |
| image_batch_size=mask_image.shape[0] |
| else: |
| assertFalse |
| |
| ifimage_batch_size!=1andbatch_size!=image_batch_size: |
Problem:
The validators treat np.ndarray.shape[0] as batch size for every NumPy image. A normal single image shaped (H, W, C) is interpreted as batch size H, even though the preprocessors can handle single HWC arrays.
Impact:
Users passing valid single NumPy images get a misleading batch-size error. Fast tests only cover tensor batches and slow tests use PIL images, so this path is untested.
Reproduction:
importnumpyasnpimporttorchfromdiffusersimportDDPMScheduler, IFImg2ImgPipeline, UNet2DConditionModelunet=UNet2DConditionModel(
sample_size=8, in_channels=3, out_channels=6, layers_per_block=1,
block_out_channels=(8,), down_block_types=("CrossAttnDownBlock2D",),
up_block_types=("CrossAttnUpBlock2D",), cross_attention_dim=4,
attention_head_dim=4, norm_num_groups=1,
)
pipe=IFImg2ImgPipeline(None, None, unet, DDPMScheduler(num_train_timesteps=10, variance_type="learned_range"), None, None, None, False)
pipe.check_inputs(
prompt=None,
image=np.zeros((8, 8, 3), dtype=np.float32),
batch_size=1,
callback_steps=1,
prompt_embeds=torch.zeros(1, 77, 4),
negative_prompt_embeds=torch.zeros(1, 77, 4),
)Relevant precedent:
The local preprocessors already wrap non-list NumPy images and would handle HWC as a single image if validation allowed it.
Suggested fix:
def_image_batch_size(image):
ifisinstance(image, list):
returnlen(image)
ifisinstance(image, PIL.Image.Image):
return1ifisinstance(image, np.ndarray):
returnimage.shape[0] ifimage.ndim==4else1ifisinstance(image, torch.Tensor):
returnimage.shape[0] ifimage.ndim==4else1raiseTypeError(type(image))
Issue 2: Batched NumPy masks are converted to 5D tensors
Affected code:
| defpreprocess_mask_image(self, mask_image) ->torch.Tensor: |
| ifnotisinstance(mask_image, list): |
| mask_image= [mask_image] |
| |
| ifisinstance(mask_image[0], torch.Tensor): |
| mask_image=torch.cat(mask_image, axis=0) ifmask_image[0].ndim==4elsetorch.stack(mask_image, axis=0) |
| |
| ifmask_image.ndim==2: |
| # Batch and add channel dim for single mask |
| mask_image=mask_image.unsqueeze(0).unsqueeze(0) |
| elifmask_image.ndim==3andmask_image.shape[0] ==1: |
| # Single mask, the 0'th dimension is considered to be |
| # the existing batch size of 1 |
| mask_image=mask_image.unsqueeze(0) |
| elifmask_image.ndim==3andmask_image.shape[0] !=1: |
| # Batch of mask, the 0'th dimension is considered to be |
| # the batching dimension |
| mask_image=mask_image.unsqueeze(1) |
| |
| mask_image[mask_image<0.5] =0 |
| mask_image[mask_image>=0.5] =1 |
| |
| elifisinstance(mask_image[0], PIL.Image.Image): |
| new_mask_image= [] |
| |
| formask_image_inmask_image: |
| mask_image_=mask_image_.convert("L") |
| mask_image_=resize(mask_image_, self.unet.config.sample_size) |
| mask_image_=np.array(mask_image_) |
| mask_image_=mask_image_[None, None, :] |
| new_mask_image.append(mask_image_) |
| |
| mask_image=new_mask_image |
| |
| mask_image=np.concatenate(mask_image, axis=0) |
| mask_image=mask_image.astype(np.float32) /255.0 |
| mask_image[mask_image<0.5] =0 |
| mask_image[mask_image>=0.5] =1 |
| mask_image=torch.from_numpy(mask_image) |
| |
| elifisinstance(mask_image[0], np.ndarray): |
| mask_image=np.concatenate([m[None, None, :] forminmask_image], axis=0) |
| |
| mask_image[mask_image<0.5] =0 |
| mask_image[mask_image>=0.5] =1 |
| mask_image=torch.from_numpy(mask_image) |
| # Copied from diffusers.pipelines.deepfloyd_if.pipeline_if_inpainting.IFInpaintingPipeline.preprocess_mask_image |
| defpreprocess_mask_image(self, mask_image) ->torch.Tensor: |
| ifnotisinstance(mask_image, list): |
| mask_image= [mask_image] |
| |
| ifisinstance(mask_image[0], torch.Tensor): |
| mask_image=torch.cat(mask_image, axis=0) ifmask_image[0].ndim==4elsetorch.stack(mask_image, axis=0) |
| |
| ifmask_image.ndim==2: |
| # Batch and add channel dim for single mask |
| mask_image=mask_image.unsqueeze(0).unsqueeze(0) |
| elifmask_image.ndim==3andmask_image.shape[0] ==1: |
| # Single mask, the 0'th dimension is considered to be |
| # the existing batch size of 1 |
| mask_image=mask_image.unsqueeze(0) |
| elifmask_image.ndim==3andmask_image.shape[0] !=1: |
| # Batch of mask, the 0'th dimension is considered to be |
| # the batching dimension |
| mask_image=mask_image.unsqueeze(1) |
| |
| mask_image[mask_image<0.5] =0 |
| mask_image[mask_image>=0.5] =1 |
| |
| elifisinstance(mask_image[0], PIL.Image.Image): |
| new_mask_image= [] |
| |
| formask_image_inmask_image: |
| mask_image_=mask_image_.convert("L") |
| mask_image_=resize(mask_image_, self.unet.config.sample_size) |
| mask_image_=np.array(mask_image_) |
| mask_image_=mask_image_[None, None, :] |
| new_mask_image.append(mask_image_) |
| |
| mask_image=new_mask_image |
| |
| mask_image=np.concatenate(mask_image, axis=0) |
| mask_image=mask_image.astype(np.float32) /255.0 |
| mask_image[mask_image<0.5] =0 |
| mask_image[mask_image>=0.5] =1 |
| mask_image=torch.from_numpy(mask_image) |
| |
| elifisinstance(mask_image[0], np.ndarray): |
| mask_image=np.concatenate([m[None, None, :] forminmask_image], axis=0) |
| |
| mask_image[mask_image<0.5] =0 |
| mask_image[mask_image>=0.5] =1 |
| mask_image=torch.from_numpy(mask_image) |
Problem:
preprocess_mask_image() wraps a non-list NumPy mask in a list, then blindly applies m[None, None, :]. For a batched mask shaped (B, H, W), this returns (1, 1, B, H, W) instead of (B, 1, H, W).
Impact:
Batched NumPy masks either fail later during broadcasting or apply the mask with the wrong shape. This is not covered by the current tests.
Reproduction:
importnumpyasnpfromdiffusersimportDDPMScheduler, IFInpaintingPipeline, UNet2DConditionModelunet=UNet2DConditionModel(
sample_size=8, in_channels=3, out_channels=6, layers_per_block=1,
block_out_channels=(8,), down_block_types=("CrossAttnDownBlock2D",),
up_block_types=("CrossAttnUpBlock2D",), cross_attention_dim=4,
attention_head_dim=4, norm_num_groups=1,
)
pipe=IFInpaintingPipeline(None, None, unet, DDPMScheduler(num_train_timesteps=10, variance_type="learned_range"), None, None, None, False)
mask=np.zeros((2, 8, 8), dtype=np.float32)
print(pipe.preprocess_mask_image(mask).shape) # torch.Size([1, 1, 2, 8, 8])Relevant precedent:
Tensor masks in the same method distinguish 2D single masks from 3D batched masks before adding channel dimensions.
Suggested fix:
elifisinstance(mask_image[0], np.ndarray):
mask_image=np.stack(mask_image, axis=0) iflen(mask_image) >1elsemask_image[0]
ifmask_image.ndim==2:
mask_image=mask_image[None, None, :, :]
elifmask_image.ndim==3:
mask_image=mask_image[:, None, :, :]
elifmask_image.ndim==4andmask_image.shape[-1] ==1:
mask_image=mask_image.transpose(0, 3, 1, 2)
else:
raiseValueError(f"Unsupported mask shape: {mask_image.shape}")
mask_image= (mask_image>=0.5).astype(np.float32)
mask_image=torch.from_numpy(mask_image)Issue 3: strength is documented as constrained but invalid values are accepted
Affected code:
| defcheck_inputs( |
| self, |
| prompt, |
| image, |
| batch_size, |
| callback_steps, |
| negative_prompt=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| ): |
| if (callback_stepsisNone) or ( |
| callback_stepsisnotNoneand (notisinstance(callback_steps, int) orcallback_steps<=0) |
| ): |
| raiseValueError( |
| f"`callback_steps` has to be a positive integer but is {callback_steps} of type" |
| f" {type(callback_steps)}." |
| ) |
| |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisNoneandprompt_embedsisNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| |
| ifnegative_promptisnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandnegative_prompt_embedsisnotNone: |
| ifprompt_embeds.shape!=negative_prompt_embeds.shape: |
| raiseValueError( |
| "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" |
| f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" |
| f" {negative_prompt_embeds.shape}." |
| ) |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.StableDiffusionImg2ImgPipeline.get_timesteps |
| defget_timesteps(self, num_inference_steps, strength): |
| # get the original timestep using init_timestep |
| init_timestep=min(int(num_inference_steps*strength), num_inference_steps) |
| |
| t_start=max(num_inference_steps-init_timestep, 0) |
| timesteps=self.scheduler.timesteps[t_start*self.scheduler.order :] |
| ifhasattr(self.scheduler, "set_begin_index"): |
| self.scheduler.set_begin_index(t_start*self.scheduler.order) |
| |
| returntimesteps, num_inference_steps-t_start |
| defcheck_inputs( |
| self, |
| prompt, |
| image, |
| mask_image, |
| batch_size, |
| callback_steps, |
| negative_prompt=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| ): |
| if (callback_stepsisNone) or ( |
| callback_stepsisnotNoneand (notisinstance(callback_steps, int) orcallback_steps<=0) |
| ): |
| raiseValueError( |
| f"`callback_steps` has to be a positive integer but is {callback_steps} of type" |
| f" {type(callback_steps)}." |
| ) |
| |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisNoneandprompt_embedsisNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| |
| ifnegative_promptisnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandnegative_prompt_embedsisnotNone: |
| ifprompt_embeds.shape!=negative_prompt_embeds.shape: |
| raiseValueError( |
| "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" |
| f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" |
| f" {negative_prompt_embeds.shape}." |
| ) |
| |
| # image |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| |
| # mask_image |
| |
| ifisinstance(mask_image, list): |
| check_image_type=mask_image[0] |
| else: |
| check_image_type=mask_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`mask_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(mask_image, list): |
| image_batch_size=len(mask_image) |
| elifisinstance(mask_image, torch.Tensor): |
| image_batch_size=mask_image.shape[0] |
| elifisinstance(mask_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(mask_image, np.ndarray): |
| image_batch_size=mask_image.shape[0] |
| else: |
| assertFalse |
| |
| ifimage_batch_size!=1andbatch_size!=image_batch_size: |
| raiseValueError( |
| f"mask_image batch size: {image_batch_size} must be `1` or the same as prompt batch size {batch_size}" |
| ) |
| defcheck_inputs( |
| self, |
| prompt, |
| image, |
| original_image, |
| batch_size, |
| callback_steps, |
| negative_prompt=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| ): |
| if (callback_stepsisNone) or ( |
| callback_stepsisnotNoneand (notisinstance(callback_steps, int) orcallback_steps<=0) |
| ): |
| raiseValueError( |
| f"`callback_steps` has to be a positive integer but is {callback_steps} of type" |
| f" {type(callback_steps)}." |
| ) |
| |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisNoneandprompt_embedsisNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| |
| ifnegative_promptisnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandnegative_prompt_embedsisnotNone: |
| ifprompt_embeds.shape!=negative_prompt_embeds.shape: |
| raiseValueError( |
| "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" |
| f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" |
| f" {negative_prompt_embeds.shape}." |
| ) |
| |
| # image |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| |
| # original_image |
| |
| ifisinstance(original_image, list): |
| check_image_type=original_image[0] |
| else: |
| check_image_type=original_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`original_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(original_image, list): |
| image_batch_size=len(original_image) |
| elifisinstance(original_image, torch.Tensor): |
| image_batch_size=original_image.shape[0] |
| elifisinstance(original_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(original_image, np.ndarray): |
| image_batch_size=original_image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError( |
| f"original_image batch size: {image_batch_size} must be same as prompt batch size {batch_size}" |
| ) |
| defcheck_inputs( |
| self, |
| prompt, |
| image, |
| original_image, |
| mask_image, |
| batch_size, |
| callback_steps, |
| negative_prompt=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| ): |
| if (callback_stepsisNone) or ( |
| callback_stepsisnotNoneand (notisinstance(callback_steps, int) orcallback_steps<=0) |
| ): |
| raiseValueError( |
| f"`callback_steps` has to be a positive integer but is {callback_steps} of type" |
| f" {type(callback_steps)}." |
| ) |
| |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisNoneandprompt_embedsisNone: |
| raiseValueError( |
| "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| |
| ifnegative_promptisnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandnegative_prompt_embedsisnotNone: |
| ifprompt_embeds.shape!=negative_prompt_embeds.shape: |
| raiseValueError( |
| "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but" |
| f" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`" |
| f" {negative_prompt_embeds.shape}." |
| ) |
| |
| # image |
| |
| ifisinstance(image, list): |
| check_image_type=image[0] |
| else: |
| check_image_type=image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(image, list): |
| image_batch_size=len(image) |
| elifisinstance(image, torch.Tensor): |
| image_batch_size=image.shape[0] |
| elifisinstance(image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(image, np.ndarray): |
| image_batch_size=image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError(f"image batch size: {image_batch_size} must be same as prompt batch size {batch_size}") |
| |
| # original_image |
| |
| ifisinstance(original_image, list): |
| check_image_type=original_image[0] |
| else: |
| check_image_type=original_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`original_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(original_image, list): |
| image_batch_size=len(original_image) |
| elifisinstance(original_image, torch.Tensor): |
| image_batch_size=original_image.shape[0] |
| elifisinstance(original_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(original_image, np.ndarray): |
| image_batch_size=original_image.shape[0] |
| else: |
| assertFalse |
| |
| ifbatch_size!=image_batch_size: |
| raiseValueError( |
| f"original_image batch size: {image_batch_size} must be same as prompt batch size {batch_size}" |
| ) |
| |
| # mask_image |
| |
| ifisinstance(mask_image, list): |
| check_image_type=mask_image[0] |
| else: |
| check_image_type=mask_image |
| |
| if ( |
| notisinstance(check_image_type, torch.Tensor) |
| andnotisinstance(check_image_type, PIL.Image.Image) |
| andnotisinstance(check_image_type, np.ndarray) |
| ): |
| raiseValueError( |
| "`mask_image` has to be of type `torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, or list[...] but is" |
| f" {type(check_image_type)}" |
| ) |
| |
| ifisinstance(mask_image, list): |
| image_batch_size=len(mask_image) |
| elifisinstance(mask_image, torch.Tensor): |
| image_batch_size=mask_image.shape[0] |
| elifisinstance(mask_image, PIL.Image.Image): |
| image_batch_size=1 |
| elifisinstance(mask_image, np.ndarray): |
| image_batch_size=mask_image.shape[0] |
| else: |
| assertFalse |
| |
| ifimage_batch_size!=1andbatch_size!=image_batch_size: |
Problem:
The docs say strength must be between 0 and 1, but the four img2img/inpainting variants never validate it. Negative values can silently produce empty outputs, and values above 1 are clamped by timestep math and behave like 1.
Impact:
Invalid user input produces surprising generation behavior instead of a clear ValueError.
Reproduction:
importtorchfromdiffusersimportDDPMScheduler, IFImg2ImgPipeline, UNet2DConditionModelunet=UNet2DConditionModel(
sample_size=8, in_channels=3, out_channels=6, layers_per_block=1,
block_out_channels=(8,), down_block_types=("CrossAttnDownBlock2D",),
up_block_types=("CrossAttnUpBlock2D",), cross_attention_dim=4,
attention_head_dim=4, norm_num_groups=1,
)
pipe=IFImg2ImgPipeline(None, None, unet, DDPMScheduler(num_train_timesteps=10, variance_type="learned_range"), None, None, None, False)
embeds=torch.zeros(1, 77, 4)
image=torch.zeros(1, 3, 8, 8)
out=pipe(prompt_embeds=embeds, negative_prompt_embeds=embeds, image=image, strength=-0.1, num_inference_steps=2, output_type="pt")
print(out.images.shape) # torch.Size([0, 3, 8, 8])Relevant precedent:
| defcheck_inputs( |
| self, |
| prompt, |
| strength, |
| callback_steps, |
| negative_prompt=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| ip_adapter_image=None, |
| ip_adapter_image_embeds=None, |
| callback_on_step_end_tensor_inputs=None, |
| ): |
| ifstrength<0orstrength>1: |
| raiseValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") |
| |
Suggested fix:
defcheck_inputs(..., strength, ...):
ifstrength<0orstrength>1:
raiseValueError(f"The value of `strength` should be in [0.0, 1.0], but is {strength}")Issue 4: IFPipelineOutput is hidden behind torch+transformers lazy-import guards
Affected code:
| ] |
| } |
| |
| try: |
| ifnot (is_transformers_available() andis_torch_available()): |
| raiseOptionalDependencyNotAvailable() |
| exceptOptionalDependencyNotAvailable: |
| from ...utilsimportdummy_torch_and_transformers_objects# noqa F403 |
| |
| _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects)) |
| else: |
| _import_structure["pipeline_if"] = ["IFPipeline"] |
| _import_structure["pipeline_if_img2img"] = ["IFImg2ImgPipeline"] |
| _import_structure["pipeline_if_img2img_superresolution"] = ["IFImg2ImgSuperResolutionPipeline"] |
| _import_structure["pipeline_if_inpainting"] = ["IFInpaintingPipeline"] |
| _import_structure["pipeline_if_inpainting_superresolution"] = ["IFInpaintingSuperResolutionPipeline"] |
| _import_structure["pipeline_if_superresolution"] = ["IFSuperResolutionPipeline"] |
| _import_structure["pipeline_output"] = ["IFPipelineOutput"] |
| _import_structure["safety_checker"] = ["IFSafetyChecker"] |
| _import_structure["watermark"] = ["IFWatermarker"] |
Problem:
IFPipelineOutput is a lightweight dataclass that only needs NumPy/PIL/BaseOutput, but it is added to _import_structure only when both torch and transformers are available.
Impact:
In dependency-light environments, users cannot import an output type that does not require the missing dependency.
Reproduction:
importimportlibimportsysimportdiffusers.utils.import_utilsasiuiu._transformers_available=Falsefornameinlist(sys.modules):
ifname.startswith("diffusers.pipelines.deepfloyd_if"):
delsys.modules[name]
module=importlib.import_module("diffusers.pipelines.deepfloyd_if")
print(hasattr(module, "IFPipelineOutput")) # Falsefromdiffusers.pipelines.deepfloyd_ifimportIFPipelineOutput# ImportErrorRelevant precedent:
The timesteps constants in the same __init__.py are already exported outside the torch+transformers guard.
Suggested fix:
_import_structure= {
"timesteps": [...],
"pipeline_output": ["IFPipelineOutput"],
}Issue 5: encode_prompt() detaches gradients in all IF pipelines
Affected code:
| @torch.no_grad() |
| defencode_prompt( |
| self, |
| prompt: str|list[str], |
| do_classifier_free_guidance: bool=True, |
| num_images_per_prompt: int=1, |
| device: torch.device|None=None, |
| negative_prompt: str|list[str] |None=None, |
| prompt_embeds: torch.Tensor|None=None, |
| negative_prompt_embeds: torch.Tensor|None=None, |
| clean_caption: bool=False, |
| ): |
| r""" |
| Encodes the prompt into text encoder hidden states. |
| |
| Args: |
| prompt (`str` or `list[str]`, *optional*): |
| prompt to be encoded |
| do_classifier_free_guidance (`bool`, *optional*, defaults to `True`): |
| whether to use classifier free guidance or not |
| num_images_per_prompt (`int`, *optional*, defaults to 1): |
| number of images that should be generated per prompt |
| device: (`torch.device`, *optional*): |
| torch device to place the resulting embeddings on |
| negative_prompt (`str` or `list[str]`, *optional*): |
| The prompt or prompts not to guide the image generation. If not defined, one has to pass |
| `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. |
| Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). |
| prompt_embeds (`torch.Tensor`, *optional*): |
| Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not |
| provided, text embeddings will be generated from `prompt` input argument. |
| negative_prompt_embeds (`torch.Tensor`, *optional*): |
| Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt |
| weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input |
| argument. |
| clean_caption (bool, defaults to `False`): |
| If `True`, the function will preprocess and clean the provided caption before encoding. |
| """ |
| ifpromptisnotNoneandnegative_promptisnotNone: |
| iftype(prompt) isnottype(negative_prompt): |
| raiseTypeError( |
| f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" |
| f" {type(prompt)}." |
| ) |
| |
| ifdeviceisNone: |
| device=self._execution_device |
| |
| ifpromptisnotNoneandisinstance(prompt, str): |
| batch_size=1 |
| elifpromptisnotNoneandisinstance(prompt, list): |
| batch_size=len(prompt) |
| else: |
| batch_size=prompt_embeds.shape[0] |
| |
| # while T5 can handle much longer input sequences than 77, the text encoder was trained with a max length of 77 for IF |
| max_length=77 |
| |
| ifprompt_embedsisNone: |
| prompt=self._text_preprocessing(prompt, clean_caption=clean_caption) |
| text_inputs=self.tokenizer( |
| prompt, |
| padding="max_length", |
| max_length=max_length, |
| truncation=True, |
| add_special_tokens=True, |
| return_tensors="pt", |
| ) |
| text_input_ids=text_inputs.input_ids |
| untruncated_ids=self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids |
| |
| ifuntruncated_ids.shape[-1] >=text_input_ids.shape[-1] andnottorch.equal( |
| text_input_ids, untruncated_ids |
| ): |
| removed_text=self.tokenizer.batch_decode(untruncated_ids[:, max_length-1 : -1]) |
| logger.warning( |
| "The following part of your input was truncated because CLIP can only handle sequences up to" |
| f" {max_length} tokens: {removed_text}" |
| ) |
| |
| attention_mask=text_inputs.attention_mask.to(device) |
| |
| prompt_embeds=self.text_encoder( |
| text_input_ids.to(device), |
| attention_mask=attention_mask, |
| ) |
| prompt_embeds=prompt_embeds[0] |
| |
| ifself.text_encoderisnotNone: |
| dtype=self.text_encoder.dtype |
| elifself.unetisnotNone: |
| dtype=self.unet.dtype |
| else: |
| dtype=None |
| |
| prompt_embeds=prompt_embeds.to(dtype=dtype, device=device) |
| |
| bs_embed, seq_len, _=prompt_embeds.shape |
| # duplicate text embeddings for each generation per prompt, using mps friendly method |
| prompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1) |
| prompt_embeds=prompt_embeds.view(bs_embed*num_images_per_prompt, seq_len, -1) |
| |
| # get unconditional embeddings for classifier free guidance |
| ifdo_classifier_free_guidanceandnegative_prompt_embedsisNone: |
| uncond_tokens: list[str] |
| ifnegative_promptisNone: |
| uncond_tokens= [""] *batch_size |
| elifisinstance(negative_prompt, str): |
| uncond_tokens= [negative_prompt] |
| elifbatch_size!=len(negative_prompt): |
| raiseValueError( |
| f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" |
| f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" |
| " the batch size of `prompt`." |
| ) |
| else: |
| uncond_tokens=negative_prompt |
| |
| uncond_tokens=self._text_preprocessing(uncond_tokens, clean_caption=clean_caption) |
| max_length=prompt_embeds.shape[1] |
| uncond_input=self.tokenizer( |
| uncond_tokens, |
| padding="max_length", |
| max_length=max_length, |
| truncation=True, |
| return_attention_mask=True, |
| add_special_tokens=True, |
| return_tensors="pt", |
| ) |
| attention_mask=uncond_input.attention_mask.to(device) |
| |
| negative_prompt_embeds=self.text_encoder( |
| uncond_input.input_ids.to(device), |
| attention_mask=attention_mask, |
| ) |
| negative_prompt_embeds=negative_prompt_embeds[0] |
| |
| ifdo_classifier_free_guidance: |
| # duplicate unconditional embeddings for each generation per prompt, using mps friendly method |
| seq_len=negative_prompt_embeds.shape[1] |
| |
| negative_prompt_embeds=negative_prompt_embeds.to(dtype=dtype, device=device) |
| |
| negative_prompt_embeds=negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) |
| negative_prompt_embeds=negative_prompt_embeds.view(batch_size*num_images_per_prompt, seq_len, -1) |
| |
| # For classifier free guidance, we need to do two forward passes. |
| # Here we concatenate the unconditional and text embeddings into a single batch |
| # to avoid doing two forward passes |
| else: |
| negative_prompt_embeds=None |
| |
| returnprompt_embeds, negative_prompt_embeds |
| @torch.no_grad() |
| defencode_prompt( |
| self, |
| prompt: str|list[str], |
| do_classifier_free_guidance: bool=True, |
| num_images_per_prompt: int=1, |
| device: torch.device|None=None, |
| negative_prompt: str|list[str] |None=None, |
| prompt_embeds: torch.Tensor|None=None, |
| negative_prompt_embeds: torch.Tensor|None=None, |
| clean_caption: bool=False, |
| ): |
| r""" |
| Encodes the prompt into text encoder hidden states. |
| |
| Args: |
| prompt (`str` or `list[str]`, *optional*): |
| prompt to be encoded |
| do_classifier_free_guidance (`bool`, *optional*, defaults to `True`): |
| whether to use classifier free guidance or not |
| num_images_per_prompt (`int`, *optional*, defaults to 1): |
| number of images that should be generated per prompt |
| device: (`torch.device`, *optional*): |
| torch device to place the resulting embeddings on |
| negative_prompt (`str` or `list[str]`, *optional*): |
| The prompt or prompts not to guide the image generation. If not defined, one has to pass |
| `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. |
| Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). |
| prompt_embeds (`torch.Tensor`, *optional*): |
| Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not |
| provided, text embeddings will be generated from `prompt` input argument. |
| negative_prompt_embeds (`torch.Tensor`, *optional*): |
| Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt |
| weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input |
| argument. |
| clean_caption (bool, defaults to `False`): |
| If `True`, the function will preprocess and clean the provided caption before encoding. |
| """ |
| ifpromptisnotNoneandnegative_promptisnotNone: |
| iftype(prompt) isnottype(negative_prompt): |
| raiseTypeError( |
| f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" |
| f" {type(prompt)}." |
| ) |
| |
| ifdeviceisNone: |
| device=self._execution_device |
| |
| ifpromptisnotNoneandisinstance(prompt, str): |
| batch_size=1 |
| elifpromptisnotNoneandisinstance(prompt, list): |
| batch_size=len(prompt) |
| else: |
| batch_size=prompt_embeds.shape[0] |
| |
| # while T5 can handle much longer input sequences than 77, the text encoder was trained with a max length of 77 for IF |
| max_length=77 |
| |
| ifprompt_embedsisNone: |
| prompt=self._text_preprocessing(prompt, clean_caption=clean_caption) |
| text_inputs=self.tokenizer( |
| prompt, |
| padding="max_length", |
| max_length=max_length, |
| truncation=True, |
| add_special_tokens=True, |
| return_tensors="pt", |
| ) |
| text_input_ids=text_inputs.input_ids |
| untruncated_ids=self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids |
| |
| ifuntruncated_ids.shape[-1] >=text_input_ids.shape[-1] andnottorch.equal( |
| text_input_ids, untruncated_ids |
| ): |
| removed_text=self.tokenizer.batch_decode(untruncated_ids[:, max_length-1 : -1]) |
| logger.warning( |
| "The following part of your input was truncated because CLIP can only handle sequences up to" |
| f" {max_length} tokens: {removed_text}" |
| ) |
| |
| attention_mask=text_inputs.attention_mask.to(device) |
| |
| prompt_embeds=self.text_encoder( |
| text_input_ids.to(device), |
| attention_mask=attention_mask, |
| ) |
| prompt_embeds=prompt_embeds[0] |
| |
| ifself.text_encoderisnotNone: |
| dtype=self.text_encoder.dtype |
| elifself.unetisnotNone: |
| dtype=self.unet.dtype |
| else: |
| dtype=None |
| |
| prompt_embeds=prompt_embeds.to(dtype=dtype, device=device) |
| |
| bs_embed, seq_len, _=prompt_embeds.shape |
| # duplicate text embeddings for each generation per prompt, using mps friendly method |
| prompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1) |
| prompt_embeds=prompt_embeds.view(bs_embed*num_images_per_prompt, seq_len, -1) |
| |
| # get unconditional embeddings for classifier free guidance |
| ifdo_classifier_free_guidanceandnegative_prompt_embedsisNone: |
| uncond_tokens: list[str] |
| ifnegative_promptisNone: |
| uncond_tokens= [""] *batch_size |
| elifisinstance(negative_prompt, str): |
| uncond_tokens= [negative_prompt] |
| elifbatch_size!=len(negative_prompt): |
| raiseValueError( |
| f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" |
| f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" |
| " the batch size of `prompt`." |
| ) |
| else: |
| uncond_tokens=negative_prompt |
| |
| uncond_tokens=self._text_preprocessing(uncond_tokens, clean_caption=clean_caption) |
| max_length=prompt_embeds.shape[1] |
| uncond_input=self.tokenizer( |
| uncond_tokens, |
| padding="max_length", |
| max_length=max_length, |
| truncation=True, |
| return_attention_mask=True, |
| add_special_tokens=True, |
| return_tensors="pt", |
| ) |
| attention_mask=uncond_input.attention_mask.to(device) |
| |
| negative_prompt_embeds=self.text_encoder( |
| uncond_input.input_ids.to(device), |
| attention_mask=attention_mask, |
| ) |
| negative_prompt_embeds=negative_prompt_embeds[0] |
| |
| ifdo_classifier_free_guidance: |
| # duplicate unconditional embeddings for each generation per prompt, using mps friendly method |
| seq_len=negative_prompt_embeds.shape[1] |
| |
| negative_prompt_embeds=negative_prompt_embeds.to(dtype=dtype, device=device) |
| @torch.no_grad() |
| # Copied from diffusers.pipelines.deepfloyd_if.pipeline_if.IFPipeline.encode_prompt |
| defencode_prompt( |
| self, |
| prompt: str|list[str], |
| do_classifier_free_guidance: bool=True, |
| num_images_per_prompt: int=1, |
| device: torch.device|None=None, |
| negative_prompt: str|list[str] |None=None, |
| prompt_embeds: torch.Tensor|None=None, |
| negative_prompt_embeds: torch.Tensor|None=None, |
| clean_caption: bool=False, |
| ): |
| r""" |
| Encodes the prompt into text encoder hidden states. |
| |
| Args: |
| prompt (`str` or `list[str]`, *optional*): |
| prompt to be encoded |
| do_classifier_free_guidance (`bool`, *optional*, defaults to `True`): |
| whether to use classifier free guidance or not |
| num_images_per_prompt (`int`, *optional*, defaults to 1): |
| number of images that should be generated per prompt |
| device: (`torch.device`, *optional*): |
| torch device to place the resulting embeddings on |
| negative_prompt (`str` or `list[str]`, *optional*): |
| The prompt or prompts not to guide the image generation. If not defined, one has to pass |
| `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead. |
| Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`). |
| prompt_embeds (`torch.Tensor`, *optional*): |
| Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not |
| provided, text embeddings will be generated from `prompt` input argument. |
| negative_prompt_embeds (`torch.Tensor`, *optional*): |
| Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt |
| weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input |
| argument. |
| clean_caption (bool, defaults to `False`): |
| If `True`, the function will preprocess and clean the provided caption before encoding. |
| """ |
| ifpromptisnotNoneandnegative_promptisnotNone: |
| iftype(prompt) isnottype(negative_prompt): |
| raiseTypeError( |
| f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" |
| f" {type(prompt)}." |
| ) |
| |
| ifdeviceisNone: |
| device=self._execution_device |
| |
| ifpromptisnotNoneandisinstance(prompt, str): |
| batch_size=1 |
| elifpromptisnotNoneandisinstance(prompt, list): |
| batch_size=len(prompt) |
| else: |
| batch_size=prompt_embeds.shape[0] |
| |
| # while T5 can handle much longer input sequences than 77, the text encoder was trained with a max length of 77 for IF |
| max_length=77 |
| |
| ifprompt_embedsisNone: |
| prompt=self._text_preprocessing(prompt, clean_caption=clean_caption) |
| text_inputs=self.tokenizer( |
| prompt, |
| padding="max_length", |
| max_length=max_length, |
| truncation=True, |
| add_special_tokens=True, |
| return_tensors="pt", |
| ) |
| text_input_ids=text_inputs.input_ids |
| untruncated_ids=self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids |
| |
| ifuntruncated_ids.shape[-1] >=text_input_ids.shape[-1] andnottorch.equal( |
| text_input_ids, untruncated_ids |
| ): |
| removed_text=self.tokenizer.batch_decode(untruncated_ids[:, max_length-1 : -1]) |
| logger.warning( |
| "The following part of your input was truncated because CLIP can only handle sequences up to" |
| f" {max_length} tokens: {removed_text}" |
| ) |
| |
| attention_mask=text_inputs.attention_mask.to(device) |
| |
| prompt_embeds=self.text_encoder( |
| text_input_ids.to(device), |
| attention_mask=attention_mask, |
| ) |
| prompt_embeds=prompt_embeds[0] |
| |
| ifself.text_encoderisnotNone: |
| dtype=self.text_encoder.dtype |
| elifself.unetisnotNone: |
| dtype=self.unet.dtype |
| else: |
| dtype=None |
| |
| prompt_embeds=prompt_embeds.to(dtype=dtype, device=device) |
| |
| bs_embed, seq_len, _=prompt_embeds.shape |
| # duplicate text embeddings for each generation per prompt, using mps friendly method |
| prompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1) |
| prompt_embeds=prompt_embeds.view(bs_embed*num_images_per_prompt, seq_len, -1) |
| |
| # get unconditional embeddings for classifier free guidance |
| ifdo_classifier_free_guidanceandnegative_prompt_embedsisNone: |
| uncond_tokens: list[str] |
| ifnegative_promptisNone: |
| uncond_tokens= [""] *batch_size |
| elifisinstance(negative_prompt, str): |
| uncond_tokens= [negative_prompt] |
| elifbatch_size!=len(negative_prompt): |
| raiseValueError( |
| f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:" |
| f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches" |
| " the batch size of `prompt`." |
| ) |
| else: |
| uncond_tokens=negative_prompt |
| |
| uncond_tokens=self._text_preprocessing(uncond_tokens, clean_caption=clean_caption) |
| max_length=prompt_embeds.shape[1] |
| uncond_input=self.tokenizer( |
| uncond_tokens, |
| padding="max_length", |
| max_length=max_length, |
| truncation=True, |
| return_attention_mask=True, |
| add_special_tokens=True, |
| return_tensors="pt", |
| ) |
| attention_mask=uncond_input.attention_mask.to(device) |
| |
| negative_prompt_embeds=self.text_encoder( |
| uncond_input.input_ids.to(device), |
| attention_mask=attention_mask, |
| ) |
| negative_prompt_embeds=negative_prompt_embeds[0] |
| |
| ifdo_classifier_free_guidance: |
| # duplicate unconditional embeddings for each generation per prompt, using mps friendly method |
| seq_len=negative_prompt_embeds.shape[1] |
| |
| negative_prompt_embeds=negative_prompt_embeds.to(dtype=dtype, device=device) |
| |
| negative_prompt_embeds=negative_prompt_embeds.repeat(1, num_images_per_prompt, 1) |
| negative_prompt_embeds=negative_prompt_embeds.view(batch_size*num_images_per_prompt, seq_len, -1) |
| |
| # For classifier free guidance, we need to do two forward passes. |
| # Here we concatenate the unconditional and text embeddings into a single batch |
| # to avoid doing two forward passes |
| else: |
| negative_prompt_embeds=None |
| |
| returnprompt_embeds, negative_prompt_embeds |
Problem:
encode_prompt() is decorated with @torch.no_grad() across the copied IF variants. __call__() is already no-grad, so the helper-level decorator prevents advanced callers from using encode_prompt() with gradients enabled.
Impact:
Prompt-embedding optimization and training-style workflows cannot reuse the public helper without silently detaching tensors.
Reproduction:
importtorchfromdiffusersimportDDPMScheduler, IFPipeline, UNet2DConditionModelunet=UNet2DConditionModel(
sample_size=8, in_channels=3, out_channels=6, layers_per_block=1,
block_out_channels=(8,), down_block_types=("CrossAttnDownBlock2D",),
up_block_types=("CrossAttnUpBlock2D",), cross_attention_dim=4,
attention_head_dim=4, norm_num_groups=1,
)
pipe=IFPipeline(None, None, unet, DDPMScheduler(num_train_timesteps=10, variance_type="learned_range"), None, None, None, False)
x=torch.randn(1, 77, 4, requires_grad=True)
prompt_embeds, _=pipe.encode_prompt(prompt=None, do_classifier_free_guidance=False, prompt_embeds=x, num_images_per_prompt=2)
print(x.requires_grad, prompt_embeds.requires_grad) # True FalseRelevant precedent:
Suggested fix:
# Remove @torch.no_grad() from encode_prompt() in all IF pipeline copies.# Keep @torch.no_grad() on __call__().
Issue 6: T5FilmDecoder has no direct fast or slow tests
Affected code:
| classT5FilmDecoder(ModelMixin, ConfigMixin): |
| r""" |
| T5 style decoder with FiLM conditioning. |
| |
| Args: |
| input_dims (`int`, *optional*, defaults to `128`): |
| The number of input dimensions. |
| targets_length (`int`, *optional*, defaults to `256`): |
| The length of the targets. |
| d_model (`int`, *optional*, defaults to `768`): |
| Size of the input hidden states. |
| num_layers (`int`, *optional*, defaults to `12`): |
| The number of `DecoderLayer`'s to use. |
| num_heads (`int`, *optional*, defaults to `12`): |
| The number of attention heads to use. |
| d_kv (`int`, *optional*, defaults to `64`): |
| Size of the key-value projection vectors. |
| d_ff (`int`, *optional*, defaults to `2048`): |
| The number of dimensions in the intermediate feed-forward layer of `DecoderLayer`'s. |
| dropout_rate (`float`, *optional*, defaults to `0.1`): |
| Dropout probability. |
| """ |
| |
| @register_to_config |
| def__init__( |
| self, |
| input_dims: int=128, |
| targets_length: int=256, |
| max_decoder_noise_time: float=2000.0, |
| d_model: int=768, |
| num_layers: int=12, |
| num_heads: int=12, |
| d_kv: int=64, |
| d_ff: int=2048, |
| dropout_rate: float=0.1, |
| ): |
| super().__init__() |
| |
| self.conditioning_emb=nn.Sequential( |
| nn.Linear(d_model, d_model*4, bias=False), |
| nn.SiLU(), |
| nn.Linear(d_model*4, d_model*4, bias=False), |
| nn.SiLU(), |
| ) |
| |
| self.position_encoding=nn.Embedding(targets_length, d_model) |
| self.position_encoding.weight.requires_grad=False |
| |
| self.continuous_inputs_projection=nn.Linear(input_dims, d_model, bias=False) |
| |
| self.dropout=nn.Dropout(p=dropout_rate) |
| |
| self.decoders=nn.ModuleList() |
| forlyr_numinrange(num_layers): |
| # FiLM conditional T5 decoder |
| lyr=DecoderLayer(d_model=d_model, d_kv=d_kv, num_heads=num_heads, d_ff=d_ff, dropout_rate=dropout_rate) |
| self.decoders.append(lyr) |
| |
| self.decoder_norm=T5LayerNorm(d_model) |
| |
| self.post_dropout=nn.Dropout(p=dropout_rate) |
| self.spec_out=nn.Linear(d_model, input_dims, bias=False) |
| |
| defencoder_decoder_mask(self, query_input: torch.Tensor, key_input: torch.Tensor) ->torch.Tensor: |
| mask=torch.mul(query_input.unsqueeze(-1), key_input.unsqueeze(-2)) |
| returnmask.unsqueeze(-3) |
| |
| defforward(self, encodings_and_masks, decoder_input_tokens, decoder_noise_time): |
| batch, _, _=decoder_input_tokens.shape |
| assertdecoder_noise_time.shape== (batch,) |
| |
| # decoder_noise_time is in [0, 1), so rescale to expected timing range. |
| time_steps=get_timestep_embedding( |
| decoder_noise_time*self.config.max_decoder_noise_time, |
| embedding_dim=self.config.d_model, |
| max_period=self.config.max_decoder_noise_time, |
| ).to(dtype=self.dtype) |
| |
| conditioning_emb=self.conditioning_emb(time_steps).unsqueeze(1) |
| |
| assertconditioning_emb.shape== (batch, 1, self.config.d_model*4) |
| |
| seq_length=decoder_input_tokens.shape[1] |
| |
| # If we want to use relative positions for audio context, we can just offset |
| # this sequence by the length of encodings_and_masks. |
| decoder_positions=torch.broadcast_to( |
| torch.arange(seq_length, device=decoder_input_tokens.device), |
| (batch, seq_length), |
| ) |
| |
| position_encodings=self.position_encoding(decoder_positions) |
| |
| inputs=self.continuous_inputs_projection(decoder_input_tokens) |
| inputs+=position_encodings |
| y=self.dropout(inputs) |
| |
| # decoder: No padding present. |
| decoder_mask=torch.ones( |
| decoder_input_tokens.shape[:2], device=decoder_input_tokens.device, dtype=inputs.dtype |
| ) |
| |
| # Translate encoding masks to encoder-decoder masks. |
| encodings_and_encdec_masks= [(x, self.encoder_decoder_mask(decoder_mask, y)) forx, yinencodings_and_masks] |
| |
| # cross attend style: concat encodings |
| encoded=torch.cat([x[0] forxinencodings_and_encdec_masks], dim=1) |
| encoder_decoder_mask=torch.cat([x[1] forxinencodings_and_encdec_masks], dim=-1) |
| |
| forlyrinself.decoders: |
| y=lyr( |
| y, |
| conditioning_emb=conditioning_emb, |
| encoder_hidden_states=encoded, |
| encoder_attention_mask=encoder_decoder_mask, |
| )[0] |
| |
| y=self.decoder_norm(y) |
| y=self.post_dropout(y) |
| |
| spec_out=self.spec_out(y) |
| returnspec_out |
Problem:
No tests under tests/ mention T5FilmDecoder. This leaves config serialization, save/load, forward shape, dtype behavior, and attention processor behavior unexercised for the model file in scope.
Impact:
Regressions in the model can land without fast model-test coverage or slow checkpoint smoke coverage.
Reproduction:
frompathlibimportPathhits= [str(p) forpinPath("tests").rglob("*.py") if"T5FilmDecoder"inp.read_text(encoding="utf-8")]
print(hits) # []asserthits, "No tests mention T5FilmDecoder"Relevant precedent:
Other transformer model families have direct tests under tests/models/transformers/.
Suggested fix:
Add a small tests/models/transformers/test_models_t5_film_transformer.py covering tiny config construction, forward pass, save/load, and dtype/device movement. Add slow coverage only if there is a maintained pretrained T5FilmDecoder checkpoint to smoke-test.
deepfloyd_ifmodel/pipeline reviewCommit tested:
0f1abc4ae8b0eb2a3b40e82a310507281144c423Review performed against the repository review rules. Note:
.ai/review-rules.mdreferencesAGENTS.md, but that file is absent in this checkout; the remaining referenced rule files were applied.Duplicate search status: searched GitHub Issues and PRs for
deepfloyd_if, affected class/function names, NumPy image/mask failures,IFPipelineOutputimport behavior, strength validation, andT5FilmDecodercoverage. I found no likely duplicates.Issue 1: Unbatched NumPy image inputs are rejected as batch-size mismatches
Affected code:
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_img2img.py
Lines 422 to 449 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_superresolution.py
Lines 539 to 566 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_inpainting.py
Lines 427 to 489 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_img2img_superresolution.py
Lines 576 to 638 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_inpainting_superresolution.py
Lines 576 to 671 in 0f1abc4
Problem:
The validators treat
np.ndarray.shape[0]as batch size for every NumPy image. A normal single image shaped(H, W, C)is interpreted as batch sizeH, even though the preprocessors can handle single HWC arrays.Impact:
Users passing valid single NumPy images get a misleading batch-size error. Fast tests only cover tensor batches and slow tests use PIL images, so this path is untested.
Reproduction:
Relevant precedent:
The local preprocessors already wrap non-list NumPy images and would handle HWC as a single image if validation allowed it.
Suggested fix:
Issue 2: Batched NumPy masks are converted to 5D tensors
Affected code:
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_inpainting.py
Lines 668 to 713 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_inpainting_superresolution.py
Lines 745 to 791 in 0f1abc4
Problem:
preprocess_mask_image()wraps a non-list NumPy mask in a list, then blindly appliesm[None, None, :]. For a batched mask shaped(B, H, W), this returns(1, 1, B, H, W)instead of(B, 1, H, W).Impact:
Batched NumPy masks either fail later during broadcasting or apply the mask with the wrong shape. This is not covered by the current tests.
Reproduction:
Relevant precedent:
Tensor masks in the same method distinguish 2D single masks from 3D batched masks before adding channel dimensions.
Suggested fix:
Issue 3:
strengthis documented as constrained but invalid values are acceptedAffected code:
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_img2img.py
Lines 378 to 449 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_img2img.py
Lines 627 to 637 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_inpainting.py
Lines 382 to 489 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_img2img_superresolution.py
Lines 531 to 638 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_inpainting_superresolution.py
Lines 533 to 671 in 0f1abc4
Problem:
The docs say
strengthmust be between 0 and 1, but the four img2img/inpainting variants never validate it. Negative values can silently produce empty outputs, and values above 1 are clamped by timestep math and behave like 1.Impact:
Invalid user input produces surprising generation behavior instead of a clear
ValueError.Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py
Lines 656 to 670 in 0f1abc4
Suggested fix:
Issue 4:
IFPipelineOutputis hidden behind torch+transformers lazy-import guardsAffected code:
diffusers/src/diffusers/pipelines/deepfloyd_if/__init__.py
Lines 24 to 43 in 0f1abc4
Problem:
IFPipelineOutputis a lightweight dataclass that only needs NumPy/PIL/BaseOutput, but it is added to_import_structureonly when both torch and transformers are available.Impact:
In dependency-light environments, users cannot import an output type that does not require the missing dependency.
Reproduction:
Relevant precedent:
The
timestepsconstants in the same__init__.pyare already exported outside the torch+transformers guard.Suggested fix:
Issue 5:
encode_prompt()detaches gradients in all IF pipelinesAffected code:
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if.py
Lines 168 to 320 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_img2img.py
Lines 192 to 333 in 0f1abc4
diffusers/src/diffusers/pipelines/deepfloyd_if/pipeline_if_superresolution.py
Lines 302 to 455 in 0f1abc4
Problem:
encode_prompt()is decorated with@torch.no_grad()across the copied IF variants.__call__()is already no-grad, so the helper-level decorator prevents advanced callers from usingencode_prompt()with gradients enabled.Impact:
Prompt-embedding optimization and training-style workflows cannot reuse the public helper without silently detaching tensors.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/flux/pipeline_flux.py
Line 311 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux.py
Line 652 in 0f1abc4
Suggested fix:
Issue 6:
T5FilmDecoderhas no direct fast or slow testsAffected code:
diffusers/src/diffusers/models/transformers/t5_film_transformer.py
Lines 25 to 146 in 0f1abc4
Problem:
No tests under
tests/mentionT5FilmDecoder. This leaves config serialization, save/load, forward shape, dtype behavior, and attention processor behavior unexercised for the model file in scope.Impact:
Regressions in the model can land without fast model-test coverage or slow checkpoint smoke coverage.
Reproduction:
Relevant precedent:
Other transformer model families have direct tests under
tests/models/transformers/.Suggested fix:
Add a small
tests/models/transformers/test_models_t5_film_transformer.pycovering tiny config construction, forward pass, save/load, and dtype/device movement. Add slow coverage only if there is a maintained pretrainedT5FilmDecodercheckpoint to smoke-test.