flux model/pipeline review
Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423
Review performed against the repository review rules.
Duplicate search status: searched current GitHub Issues and PRs for flux, affected class/function names, and failure modes. No likely duplicates found. Broader gh searches hit rate limits after targeted searches, so remaining modular-pipeline searches were checked through the GitHub connector; related PRs such as #12272 and #13482 are not duplicates.
Issue 1: Flux IP-Adapter masks are accepted but ignored
Affected code:
| def__call__( |
| self, |
| attn: "FluxAttention", |
| hidden_states: torch.Tensor, |
| encoder_hidden_states: torch.Tensor=None, |
| attention_mask: torch.Tensor|None=None, |
| image_rotary_emb: torch.Tensor|None=None, |
| ip_hidden_states: list[torch.Tensor] |None=None, |
| ip_adapter_masks: torch.Tensor|None=None, |
| ) ->torch.Tensor: |
| batch_size=hidden_states.shape[0] |
| |
| query, key, value, encoder_query, encoder_key, encoder_value=_get_qkv_projections( |
| attn, hidden_states, encoder_hidden_states |
| ) |
| |
| query=query.unflatten(-1, (attn.heads, -1)) |
| key=key.unflatten(-1, (attn.heads, -1)) |
| value=value.unflatten(-1, (attn.heads, -1)) |
| |
| query=attn.norm_q(query) |
| key=attn.norm_k(key) |
| ip_query=query |
| |
| ifencoder_hidden_statesisnotNone: |
| encoder_query=encoder_query.unflatten(-1, (attn.heads, -1)) |
| encoder_key=encoder_key.unflatten(-1, (attn.heads, -1)) |
| encoder_value=encoder_value.unflatten(-1, (attn.heads, -1)) |
| |
| encoder_query=attn.norm_added_q(encoder_query) |
| encoder_key=attn.norm_added_k(encoder_key) |
| |
| query=torch.cat([encoder_query, query], dim=1) |
| key=torch.cat([encoder_key, key], dim=1) |
| value=torch.cat([encoder_value, value], dim=1) |
| |
| ifimage_rotary_embisnotNone: |
| query=apply_rotary_emb(query, image_rotary_emb, sequence_dim=1) |
| key=apply_rotary_emb(key, image_rotary_emb, sequence_dim=1) |
| |
| hidden_states=dispatch_attention_fn( |
| query, |
| key, |
| value, |
| attn_mask=attention_mask, |
| dropout_p=0.0, |
| is_causal=False, |
| backend=self._attention_backend, |
| parallel_config=self._parallel_config, |
| ) |
| hidden_states=hidden_states.flatten(2, 3) |
| hidden_states=hidden_states.to(query.dtype) |
| |
| ifencoder_hidden_statesisnotNone: |
| encoder_hidden_states, hidden_states=hidden_states.split_with_sizes( |
| [encoder_hidden_states.shape[1], hidden_states.shape[1] -encoder_hidden_states.shape[1]], dim=1 |
| ) |
| hidden_states=attn.to_out[0](hidden_states) |
| hidden_states=attn.to_out[1](hidden_states) |
| encoder_hidden_states=attn.to_add_out(encoder_hidden_states) |
| |
| # IP-adapter |
| ip_attn_output=torch.zeros_like(hidden_states) |
| |
| forcurrent_ip_hidden_states, scale, to_k_ip, to_v_ipinzip( |
| ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip |
| ): |
| ip_key=to_k_ip(current_ip_hidden_states) |
| ip_value=to_v_ip(current_ip_hidden_states) |
| |
| ip_key=ip_key.view(batch_size, -1, attn.heads, attn.head_dim) |
| ip_value=ip_value.view(batch_size, -1, attn.heads, attn.head_dim) |
| |
| current_ip_hidden_states=dispatch_attention_fn( |
| ip_query, |
| ip_key, |
| ip_value, |
| attn_mask=None, |
| dropout_p=0.0, |
| is_causal=False, |
| backend=self._attention_backend, |
| parallel_config=self._parallel_config, |
| ) |
| current_ip_hidden_states=current_ip_hidden_states.reshape(batch_size, -1, attn.heads*attn.head_dim) |
| current_ip_hidden_states=current_ip_hidden_states.to(ip_query.dtype) |
| ip_attn_output+=scale*current_ip_hidden_states |
| quiet_attn_parameters= {"ip_adapter_masks", "ip_hidden_states"} |
| unused_kwargs= [kfork, _inkwargs.items() ifknotinattn_parametersandknotinquiet_attn_parameters] |
Problem:
FluxIPAdapterAttnProcessor.__call__ accepts ip_adapter_masks, and FluxAttention.forward explicitly quiets that kwarg, but the processor never reads or applies the masks.
Impact:
Region-specific IP-Adapter conditioning silently has no effect for Flux. Users can pass masks without warnings, making masked IP-Adapter results misleading.
Reproduction:
importtorchfromdiffusers.models.transformers.transformer_fluximportFluxAttention, FluxIPAdapterAttnProcessortorch.manual_seed(0)
attn=FluxAttention(
query_dim=4,
heads=1,
dim_head=4,
added_kv_proj_dim=4,
processor=FluxIPAdapterAttnProcessor(hidden_size=4, cross_attention_dim=4, num_tokens=(2,), scale=10.0),
)
hidden=torch.randn(1, 3, 4)
encoder=torch.randn(1, 2, 4)
ip= [torch.randn(1, 2, 4)]
mask0= [torch.zeros(1, 3, 1, 1)]
mask1= [torch.ones(1, 3, 1, 1)]
out0=attn(hidden, encoder_hidden_states=encoder, ip_hidden_states=ip, ip_adapter_masks=mask0)[0]
out1=attn(hidden, encoder_hidden_states=encoder, ip_hidden_states=ip, ip_adapter_masks=mask1)[0]
print(torch.max(torch.abs(out0-out1)).item()) # 0.0
Relevant precedent:
| ifip_adapter_masksisnotNone: |
| ifnotisinstance(ip_adapter_masks, list): |
| # for backward compatibility, we accept `ip_adapter_mask` as a tensor of shape [num_ip_adapter, 1, height, width] |
| ip_adapter_masks=list(ip_adapter_masks.unsqueeze(1)) |
| ifnot (len(ip_adapter_masks) ==len(self.scale) ==len(ip_hidden_states)): |
| raiseValueError( |
| f"Length of ip_adapter_masks array ({len(ip_adapter_masks)}) must match " |
| f"length of self.scale array ({len(self.scale)}) and number of ip_hidden_states " |
| f"({len(ip_hidden_states)})" |
| ) |
| else: |
| forindex, (mask, scale, ip_state) inenumerate(zip(ip_adapter_masks, self.scale, ip_hidden_states)): |
| ifmaskisNone: |
| continue |
| ifnotisinstance(mask, torch.Tensor) ormask.ndim!=4: |
| raiseValueError( |
| "Each element of the ip_adapter_masks array should be a tensor with shape " |
| "[1, num_images_for_ip_adapter, height, width]." |
| " Please use `IPAdapterMaskProcessor` to preprocess your mask" |
| ) |
| ifmask.shape[1] !=ip_state.shape[1]: |
| raiseValueError( |
| f"Number of masks ({mask.shape[1]}) does not match " |
| f"number of ip images ({ip_state.shape[1]}) at index {index}" |
| ) |
| ifisinstance(scale, list) andnotlen(scale) ==mask.shape[1]: |
| raiseValueError( |
| f"Number of masks ({mask.shape[1]}) does not match " |
| f"number of scales ({len(scale)}) at index {index}" |
| ) |
| else: |
| ip_adapter_masks= [None] *len(self.scale) |
| |
| # for ip-adapter |
| forcurrent_ip_hidden_states, scale, to_k_ip, to_v_ip, maskinzip( |
| ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip, ip_adapter_masks |
| ): |
| skip=False |
| ifisinstance(scale, list): |
| ifall(s==0forsinscale): |
| skip=True |
| elifscale==0: |
| skip=True |
| ifnotskip: |
| ifmaskisnotNone: |
| ifnotisinstance(scale, list): |
| scale= [scale] *mask.shape[1] |
| |
| current_num_images=mask.shape[1] |
| foriinrange(current_num_images): |
| ip_key=to_k_ip(current_ip_hidden_states[:, i, :, :]) |
| ip_value=to_v_ip(current_ip_hidden_states[:, i, :, :]) |
| |
| ip_key=attn.head_to_batch_dim(ip_key) |
| ip_value=attn.head_to_batch_dim(ip_value) |
| |
| ip_attention_probs=attn.get_attention_scores(query, ip_key, None) |
| _current_ip_hidden_states=torch.bmm(ip_attention_probs, ip_value) |
| _current_ip_hidden_states=attn.batch_to_head_dim(_current_ip_hidden_states) |
| |
| mask_downsample=IPAdapterMaskProcessor.downsample( |
| mask[:, i, :, :], |
| batch_size, |
| _current_ip_hidden_states.shape[1], |
| _current_ip_hidden_states.shape[2], |
| ) |
| |
| mask_downsample=mask_downsample.to(dtype=query.dtype, device=query.device) |
| |
| hidden_states=hidden_states+scale[i] * (_current_ip_hidden_states*mask_downsample) |
Suggested fix:
Apply the same validation/downsampling pattern used by IPAdapterAttnProcessor2_0, adapted for Flux sequence-shaped hidden states. If mask support is not intended, remove ip_adapter_masks from the quieted kwargs so users get a warning.
Issue 2: negative_prompt_embeds do not enable true CFG in several Flux pipelines
Affected code:
| do_true_cfg=true_cfg_scale>1andnegative_promptisnotNone |
| do_true_cfg=true_cfg_scale>1andnegative_promptisnotNone |
| do_true_cfg=true_cfg_scale>1andnegative_promptisnotNone |
Problem:
These pipelines compute true CFG only with negative_prompt is not None. They validate and accept negative_prompt_embeds / negative_pooled_prompt_embeds, but embeddings alone do not activate true CFG.
Impact:
Advanced users passing precomputed negative embeddings get silently different behavior from text negative prompts.
Reproduction:
importtorchfromdiffusersimportFluxImg2ImgPipelinepipe=object.__new__(FluxImg2ImgPipeline)
pipe.vae_scale_factor=8pipe._callback_tensor_inputs= ["latents", "prompt_embeds"]
pipe.check_inputs(
prompt=None,
prompt_2=None,
strength=0.5,
height=64,
width=64,
prompt_embeds=torch.zeros(2, 4, 8),
pooled_prompt_embeds=torch.zeros(2, 8),
negative_prompt_embeds=torch.zeros(2, 4, 8),
negative_pooled_prompt_embeds=torch.zeros(2, 8),
callback_on_step_end_tensor_inputs=["latents"],
max_sequence_length=48,
)
negative_prompt=Nonetrue_cfg_scale=2.0print(true_cfg_scale>1andnegative_promptisnotNone) # False
Relevant precedent:
| has_neg_prompt=negative_promptisnotNoneor ( |
| negative_prompt_embedsisnotNoneandnegative_pooled_prompt_embedsisnotNone |
| ) |
| do_true_cfg=true_cfg_scale>1andhas_neg_prompt |
| has_neg_prompt=negative_promptisnotNoneor ( |
| negative_prompt_embedsisnotNoneandnegative_pooled_prompt_embedsisnotNone |
| ) |
| do_true_cfg=true_cfg_scale>1andhas_neg_prompt |
Suggested fix:
has_neg_prompt=negative_promptisnotNoneor (
negative_prompt_embedsisnotNoneandnegative_pooled_prompt_embedsisnotNone
)
do_true_cfg=true_cfg_scale>1andhas_neg_prompt
Issue 3: Base Flux and Flux Kontext accept mismatched negative prompt embeddings
Affected code:
| 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." |
| ) |
| elifnegative_prompt_2isnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandpooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `prompt_embeds` are provided, `pooled_prompt_embeds` also have to be passed. Make sure to generate `pooled_prompt_embeds` from the same text encoder that was used to generate `prompt_embeds`." |
| ) |
| ifnegative_prompt_embedsisnotNoneandnegative_pooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `negative_prompt_embeds` are provided, `negative_pooled_prompt_embeds` also have to be passed. Make sure to generate `negative_pooled_prompt_embeds` from the same text encoder that was used to generate `negative_prompt_embeds`." |
| 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." |
| ) |
| elifnegative_prompt_2isnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandpooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `prompt_embeds` are provided, `pooled_prompt_embeds` also have to be passed. Make sure to generate `pooled_prompt_embeds` from the same text encoder that was used to generate `prompt_embeds`." |
| ) |
| ifnegative_prompt_embedsisnotNoneandnegative_pooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `negative_prompt_embeds` are provided, `negative_pooled_prompt_embeds` also have to be passed. Make sure to generate `negative_pooled_prompt_embeds` from the same text encoder that was used to generate `negative_prompt_embeds`." |
Problem:
FluxPipeline and FluxKontextPipeline check that negative pooled embeddings are provided with negative prompt embeddings, but they do not verify that negative and positive embedding shapes match.
Impact:
Shape or batch mismatches are accepted at input validation time and fail later, or worse, produce hard-to-debug true CFG behavior.
Reproduction:
importtorchfromdiffusersimportFluxPipeline, FluxKontextPipelineforclsin (FluxPipeline, FluxKontextPipeline):
pipe=object.__new__(cls)
pipe.vae_scale_factor=8pipe._callback_tensor_inputs= ["latents", "prompt_embeds"]
pipe.check_inputs(
prompt=None,
prompt_2=None,
height=64,
width=64,
prompt_embeds=torch.zeros(2, 4, 8),
pooled_prompt_embeds=torch.zeros(2, 8),
negative_prompt_embeds=torch.zeros(1, 4, 8),
negative_pooled_prompt_embeds=torch.zeros(1, 8),
callback_on_step_end_tensor_inputs=["latents"],
max_sequence_length=48,
)
print(cls.__name__, "accepted mismatched negative embeds")
Relevant precedent:
| 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}." |
| ) |
| 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}." |
| ) |
Suggested fix:
ifprompt_embedsisnotNoneandnegative_prompt_embedsisnotNone:
ifprompt_embeds.shape!=negative_prompt_embeds.shape:
raiseValueError(
"`negative_prompt_embeds` must have the same shape as `prompt_embeds`, "f"but got {negative_prompt_embeds.shape} != {prompt_embeds.shape}."
)Issue 4: Flux ControlNet img2img validates dimensions with wrong operator precedence
Affected code:
| defcheck_inputs( |
| self, |
| prompt, |
| prompt_2, |
| strength, |
| height, |
| width, |
| callback_on_step_end_tensor_inputs, |
| prompt_embeds=None, |
| pooled_prompt_embeds=None, |
| max_sequence_length=None, |
| ): |
| ifstrength<0orstrength>1: |
| raiseValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") |
| |
| ifheight%self.vae_scale_factor*2!=0orwidth%self.vae_scale_factor*2!=0: |
| logger.warning( |
| f"`height` and `width` have to be divisible by {self.vae_scale_factor*2} but are {height} and {width}. Dimensions will be resized accordingly" |
Problem:
The dimension check uses height % self.vae_scale_factor * 2, which is parsed as (height % self.vae_scale_factor) * 2, not height % (self.vae_scale_factor * 2).
Impact:
Invalid dimensions such as 72x72 pass validation when vae_scale_factor == 8, even though Flux latent packing expects dimensions divisible by 16.
Reproduction:
fromdiffusersimportFluxControlNetImg2ImgPipelinepipe=object.__new__(FluxControlNetImg2ImgPipeline)
pipe.vae_scale_factor=8pipe._callback_tensor_inputs= ["latents", "prompt_embeds", "control_image"]
pipe.check_inputs(
prompt="x",
prompt_2=None,
strength=0.5,
height=72,
width=72,
callback_on_step_end_tensor_inputs=["latents"],
prompt_embeds=None,
pooled_prompt_embeds=None,
max_sequence_length=48,
)
print(72%pipe.vae_scale_factor*2) # 0, current checkprint(72% (pipe.vae_scale_factor*2)) # 8, intended check
Relevant precedent:
| defcheck_inputs( |
| self, |
| prompt, |
| prompt_2, |
| strength, |
| height, |
| width, |
| negative_prompt=None, |
| negative_prompt_2=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| pooled_prompt_embeds=None, |
| negative_pooled_prompt_embeds=None, |
| callback_on_step_end_tensor_inputs=None, |
| max_sequence_length=None, |
| ): |
| ifstrength<0orstrength>1: |
| raiseValueError(f"The value of strength should in [0.0, 1.0] but is {strength}") |
| |
| ifheight% (self.vae_scale_factor*2) !=0orwidth% (self.vae_scale_factor*2) !=0: |
| logger.warning( |
| f"`height` and `width` have to be divisible by {self.vae_scale_factor*2} but are {height} and {width}. Dimensions will be resized accordingly" |
| defcheck_inputs( |
| self, |
| prompt, |
| prompt_2, |
| height, |
| width, |
| negative_prompt=None, |
| negative_prompt_2=None, |
| prompt_embeds=None, |
| negative_prompt_embeds=None, |
| pooled_prompt_embeds=None, |
| negative_pooled_prompt_embeds=None, |
| callback_on_step_end_tensor_inputs=None, |
| max_sequence_length=None, |
| ): |
| ifheight% (self.vae_scale_factor*2) !=0orwidth% (self.vae_scale_factor*2) !=0: |
| logger.warning( |
Suggested fix:
ifheight% (self.vae_scale_factor*2) !=0orwidth% (self.vae_scale_factor*2) !=0:
raiseValueError(...)
Issue 5: Flux Prior Redux misses tensor-batch and pooled-scale validation
Affected code:
| defcheck_inputs( |
| self, |
| image, |
| prompt, |
| prompt_2, |
| prompt_embeds=None, |
| pooled_prompt_embeds=None, |
| prompt_embeds_scale=1.0, |
| pooled_prompt_embeds_scale=1.0, |
| ): |
| ifpromptisnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifprompt_2isnotNoneandprompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `prompt_2`: {prompt_2} and `prompt_embeds`: {prompt_embeds}. Please make sure to" |
| " only forward one of the two." |
| ) |
| elifpromptisnotNoneand (notisinstance(prompt, str) andnotisinstance(prompt, list)): |
| raiseValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") |
| elifprompt_2isnotNoneand (notisinstance(prompt_2, str) andnotisinstance(prompt_2, list)): |
| raiseValueError(f"`prompt_2` has to be of type `str` or `list` but is {type(prompt_2)}") |
| ifpromptisnotNoneand (isinstance(prompt, list) andisinstance(image, list) andlen(prompt) !=len(image)): |
| raiseValueError( |
| f"number of prompts must be equal to number of images, but {len(prompt)} prompts were provided and {len(image)} images" |
| ) |
| ifprompt_embedsisnotNoneandpooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `prompt_embeds` are provided, `pooled_prompt_embeds` also have to be passed. Make sure to generate `pooled_prompt_embeds` from the same text encoder that was used to generate `prompt_embeds`." |
| ) |
| ifisinstance(prompt_embeds_scale, list) and ( |
| isinstance(image, list) andlen(prompt_embeds_scale) !=len(image) |
| ): |
| raiseValueError( |
| f"number of weights must be equal to number of images, but {len(prompt_embeds_scale)} weights were provided and {len(image)} images" |
| batch_size=1 |
| elifimageisnotNoneandisinstance(image, list): |
| batch_size=len(image) |
| else: |
| batch_size=image.shape[0] |
| ifpromptisnotNoneandisinstance(prompt, str): |
| prompt=batch_size* [prompt] |
| ifisinstance(prompt_embeds_scale, float): |
| prompt_embeds_scale=batch_size* [prompt_embeds_scale] |
| ifisinstance(pooled_prompt_embeds_scale, float): |
| pooled_prompt_embeds_scale=batch_size* [pooled_prompt_embeds_scale] |
| prompt_embeds*=torch.tensor(prompt_embeds_scale, device=device, dtype=image_embeds.dtype)[:, None, None] |
| pooled_prompt_embeds*=torch.tensor(pooled_prompt_embeds_scale, device=device, dtype=image_embeds.dtype)[ |
| :, None |
| ] |
Problem:
check_inputs only checks image batch size when image is a list. Tensor image batches are skipped. It also checks prompt_embeds_scale list length but not pooled_prompt_embeds_scale.
Impact:
Mismatched image/prompt batches and scale lengths are accepted, then fail later during tensor arithmetic or produce confusing broadcasting behavior.
Reproduction:
importtorchfromdiffusersimportFluxPriorReduxPipelinepipe=object.__new__(FluxPriorReduxPipeline)
pipe.check_inputs(
image=torch.zeros(2, 3, 32, 32),
prompt=["first", "second", "third"],
prompt_2=None,
prompt_embeds=None,
pooled_prompt_embeds=None,
prompt_embeds_scale=[1.0],
pooled_prompt_embeds_scale=[1.0, 1.0, 1.0],
)
print("accepted tensor image batch=2 with prompt batch=3 and prompt scale length=1")Relevant precedent:
Flux text/image pipelines consistently validate paired prompt embedding inputs before denoising, for example:
| 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." |
| ) |
| elifnegative_prompt_2isnotNoneandnegative_prompt_embedsisnotNone: |
| raiseValueError( |
| f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_prompt_embeds`:" |
| f" {negative_prompt_embeds}. Please make sure to only forward one of the two." |
| ) |
| |
| ifprompt_embedsisnotNoneandpooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `prompt_embeds` are provided, `pooled_prompt_embeds` also have to be passed. Make sure to generate `pooled_prompt_embeds` from the same text encoder that was used to generate `prompt_embeds`." |
| ) |
| ifnegative_prompt_embedsisnotNoneandnegative_pooled_prompt_embedsisNone: |
| raiseValueError( |
| "If `negative_prompt_embeds` are provided, `negative_pooled_prompt_embeds` also have to be passed. Make sure to generate `negative_pooled_prompt_embeds` from the same text encoder that was used to generate `negative_prompt_embeds`." |
Suggested fix:
Determine image batch size for tensors as well as lists, then validate prompt batch size and both scale arguments against that batch size.
image_batch_size=image.shape[0] ifisinstance(image, torch.Tensor) elselen(image) ifisinstance(image, list) else1forname, scalein {
"prompt_embeds_scale": prompt_embeds_scale,
"pooled_prompt_embeds_scale": pooled_prompt_embeds_scale,
}.items():
ifisinstance(scale, list) andlen(scale) !=image_batch_size:
raiseValueError(f"`{name}` must have length {image_batch_size}, but got {len(scale)}.")Issue 6: Flux modular pipelines import standard Flux pipelines and QwenImage modular helpers
Affected code:
| from ...pipelinesimportFluxPipeline |
| latents=FluxPipeline._pack_latents(latents, batch_size, num_channels_latents, height, width) |
| block_state.img_ids=FluxPipeline._prepare_latent_image_ids(None, height//2, width//2, device, dtype) |
| |
| self.set_block_state(state, block_state) |
| |
| returncomponents, state |
| |
| |
| classFluxKontextRoPEInputsStep(ModularPipelineBlocks): |
| model_name="flux-kontext" |
| |
| @property |
| defdescription(self) ->str: |
| return"Step that prepares the RoPE inputs for the denoising process of Flux Kontext. Should be placed after text encoder and latent preparation steps." |
| |
| @property |
| definputs(self) ->list[InputParam]: |
| return [ |
| InputParam(name="image_height"), |
| InputParam(name="image_width"), |
| InputParam(name="height"), |
| InputParam(name="width"), |
| InputParam(name="prompt_embeds"), |
| ] |
| |
| @property |
| defintermediate_outputs(self) ->list[OutputParam]: |
| return [ |
| OutputParam( |
| name="txt_ids", |
| kwargs_type="denoiser_input_fields", |
| type_hint=list[int], |
| description="The sequence lengths of the prompt embeds, used for RoPE calculation.", |
| ), |
| OutputParam( |
| name="img_ids", |
| kwargs_type="denoiser_input_fields", |
| type_hint=list[int], |
| description="The sequence lengths of the image latents, used for RoPE calculation.", |
| ), |
| ] |
| |
| def__call__(self, components: FluxModularPipeline, state: PipelineState) ->PipelineState: |
| block_state=self.get_block_state(state) |
| |
| prompt_embeds=block_state.prompt_embeds |
| device, dtype=prompt_embeds.device, prompt_embeds.dtype |
| block_state.txt_ids=torch.zeros(prompt_embeds.shape[1], 3).to( |
| device=prompt_embeds.device, dtype=prompt_embeds.dtype |
| ) |
| |
| img_ids=None |
| if ( |
| getattr(block_state, "image_height", None) isnotNone |
| andgetattr(block_state, "image_width", None) isnotNone |
| ): |
| image_latent_height=2* (int(block_state.image_height) // (components.vae_scale_factor*2)) |
| image_latent_width=2* (int(block_state.image_width) // (components.vae_scale_factor*2)) |
| img_ids=FluxPipeline._prepare_latent_image_ids( |
| None, image_latent_height//2, image_latent_width//2, device, dtype |
| ) |
| # image ids are the same as latent ids with the first dimension set to 1 instead of 0 |
| img_ids[..., 0] =1 |
| |
| height=2* (int(block_state.height) // (components.vae_scale_factor*2)) |
| width=2* (int(block_state.width) // (components.vae_scale_factor*2)) |
| latent_ids=FluxPipeline._prepare_latent_image_ids(None, height//2, width//2, device, dtype) |
| from ...pipelinesimportFluxPipeline |
| from ...utilsimportlogging |
| from ..modular_pipelineimportModularPipelineBlocks, PipelineState |
| from ..modular_pipeline_utilsimportInputParam, OutputParam |
| |
| # TODO: consider making these common utilities for modular if they are not pipeline-specific. |
| from ..qwenimage.inputsimportcalculate_dimension_from_latents, repeat_tensor_to_batch_size |
| image_latent_tensor=FluxPipeline._pack_latents( |
| image_latent_tensor, block_state.batch_size, image_latent_tensor.shape[1], latent_height, latent_width |
| ) |
| |
| # 3. Expand batch size |
| image_latent_tensor=repeat_tensor_to_batch_size( |
| input_name=image_latent_input_name, |
| input_tensor=image_latent_tensor, |
| num_images_per_prompt=block_state.num_images_per_prompt, |
| batch_size=block_state.batch_size, |
| ) |
| |
| setattr(block_state, image_latent_input_name, image_latent_tensor) |
| |
| # Process additional batch inputs (only batch expansion) |
| forinput_nameinself._additional_batch_inputs: |
| input_tensor=getattr(block_state, input_name) |
| ifinput_tensorisNone: |
| continue |
| |
| # Only expand batch size |
| input_tensor=repeat_tensor_to_batch_size( |
| input_name=input_name, |
| input_tensor=input_tensor, |
| num_images_per_prompt=block_state.num_images_per_prompt, |
| batch_size=block_state.batch_size, |
| ) |
| |
| setattr(block_state, input_name, input_tensor) |
| |
| self.set_block_state(state, block_state) |
| returncomponents, state |
| |
| |
| classFluxKontextAdditionalInputsStep(FluxAdditionalInputsStep): |
| model_name="flux-kontext" |
| |
| def__call__(self, components: FluxModularPipeline, state: PipelineState) ->PipelineState: |
| block_state=self.get_block_state(state) |
| |
| # Process image latent inputs (height/width calculation, patchify, and batch expansion) |
| forimage_latent_input_nameinself._image_latent_inputs: |
| image_latent_tensor=getattr(block_state, image_latent_input_name) |
| ifimage_latent_tensorisNone: |
| continue |
| |
| # 1. Calculate height/width from latents |
| # Unlike the `FluxAdditionalInputsStep`, we don't overwrite the `block.height` and `block.width` |
| height, width=calculate_dimension_from_latents(image_latent_tensor, components.vae_scale_factor) |
| ifnothasattr(block_state, "image_height"): |
| block_state.image_height=height |
| ifnothasattr(block_state, "image_width"): |
| block_state.image_width=width |
| |
| # 2. Patchify the image latent tensor |
| # TODO: Implement patchifier for Flux. |
| latent_height, latent_width=image_latent_tensor.shape[2:] |
| image_latent_tensor=FluxPipeline._pack_latents( |
| image_latent_tensor, block_state.batch_size, image_latent_tensor.shape[1], latent_height, latent_width |
| ) |
| |
| # 3. Expand batch size |
| image_latent_tensor=repeat_tensor_to_batch_size( |
| @torch.no_grad() |
| def__call__(self, components: FluxModularPipeline, state: PipelineState): |
| from ...pipelines.flux.pipeline_flux_kontextimportPREFERRED_KONTEXT_RESOLUTIONS |
Problem:
The modular review rules prohibit hidden imports from classic pipelines and cross-family modular imports. Flux modular blocks import FluxPipeline for private helper methods and import QwenImage modular input helpers.
Impact:
This creates hidden coupling between modular Flux, classic Flux, and QwenImage. Refactors or lazy-loading changes in one family can break another, and modular Flux becomes harder to serialize, test, and maintain independently.
Reproduction:
frompathlibimportPathforpathinPath("src/diffusers/modular_pipelines/flux").glob("*.py"):
forline_no, lineinenumerate(path.read_text().splitlines(), 1):
if"from ...pipelines"inlineor"from ..qwenimage"inlineor"FluxPipeline._"inline:
print(f"{path}:{line_no}: {line.strip()}")Relevant precedent:
decoders.py keeps its Flux latent unpacking helper local instead of importing the classic pipeline:
| def_unpack_latents(latents, height, width, vae_scale_factor): |
| batch_size, num_patches, channels=latents.shape |
| |
| # VAE applies 8x compression on images but we must also account for packing which requires |
| # latent height and width to be divisible by 2. |
| height=2* (int(height) // (vae_scale_factor*2)) |
| width=2* (int(width) // (vae_scale_factor*2)) |
| |
| latents=latents.view(batch_size, height//2, width//2, channels//4, 2, 2) |
| latents=latents.permute(0, 3, 1, 4, 2, 5) |
| |
| latents=latents.reshape(batch_size, channels// (2*2), height, width) |
| |
Suggested fix:
Move shared latent packing, latent id, and dimension helpers into Flux modular-local helpers or a neutral utility module that both classic and modular Flux can import without creating pipeline-family coupling. Copy the Kontext resolution table into Flux modular code or move it to a neutral Flux constants module.
Issue 7: Slow coverage is missing for most Flux pipeline variants
Affected code:
| classFluxImg2ImgPipelineFastTests(unittest.TestCase, PipelineTesterMixin, FluxIPAdapterTesterMixin): |
| classFluxInpaintPipelineFastTests(unittest.TestCase, PipelineTesterMixin, FluxIPAdapterTesterMixin): |
| classFluxFillPipelineFastTests(unittest.TestCase, PipelineTesterMixin): |
| classFluxControlPipelineFastTests(unittest.TestCase, PipelineTesterMixin): |
| classFluxControlImg2ImgPipelineFastTests(unittest.TestCase, PipelineTesterMixin): |
| classFluxControlInpaintPipelineFastTests(unittest.TestCase, PipelineTesterMixin): |
| classFluxKontextPipelineFastTests( |
| classFluxKontextInpaintPipelineFastTests( |
| classFluxControlNetImg2ImgPipelineFastTests(unittest.TestCase, PipelineTesterMixin): |
| classFluxControlNetInpaintPipelineTests(unittest.TestCase, PipelineTesterMixin): |
| classTestFluxModularPipelineFast(ModularPipelineTesterMixin): |
| pipeline_class=FluxModularPipeline |
| pipeline_blocks_class=FluxAutoBlocks |
| pretrained_model_name_or_path="hf-internal-testing/tiny-flux-modular" |
| |
| params=frozenset(["prompt", "height", "width", "guidance_scale"]) |
| batch_params=frozenset(["prompt"]) |
| expected_workflow_blocks=FLUX_TEXT2IMAGE_WORKFLOWS |
| |
| defget_dummy_inputs(self, seed=0): |
| generator=self.get_generator(seed) |
| inputs= { |
| "prompt": "A painting of a squirrel eating a burger", |
| "generator": generator, |
| "num_inference_steps": 2, |
| "guidance_scale": 5.0, |
| "height": 8, |
| "width": 8, |
| "max_sequence_length": 48, |
| "output_type": "pt", |
| } |
| returninputs |
| |
| deftest_float16_inference(self): |
| super().test_float16_inference(9e-2) |
| |
| |
| FLUX_IMAGE2IMAGE_WORKFLOWS= { |
| "image2image": [ |
| ("text_encoder", "FluxTextEncoderStep"), |
| ("vae_encoder.preprocess", "FluxProcessImagesInputStep"), |
| ("vae_encoder.encode", "FluxVaeEncoderStep"), |
| ("denoise.input.text_inputs", "FluxTextInputStep"), |
| ("denoise.input.additional_inputs", "FluxAdditionalInputsStep"), |
| ("denoise.before_denoise.prepare_latents", "FluxPrepareLatentsStep"), |
| ("denoise.before_denoise.set_timesteps", "FluxImg2ImgSetTimestepsStep"), |
| ("denoise.before_denoise.prepare_img2img_latents", "FluxImg2ImgPrepareLatentsStep"), |
| ("denoise.before_denoise.prepare_rope_inputs", "FluxRoPEInputsStep"), |
| ("denoise.denoise", "FluxDenoiseStep"), |
| ("decode", "FluxDecodeStep"), |
| ] |
| } |
| |
| |
| classTestFluxImg2ImgModularPipelineFast(ModularPipelineTesterMixin): |
| pipeline_class=FluxModularPipeline |
| pipeline_blocks_class=FluxAutoBlocks |
| pretrained_model_name_or_path="hf-internal-testing/tiny-flux-modular" |
| |
| params=frozenset(["prompt", "height", "width", "guidance_scale", "image"]) |
| batch_params=frozenset(["prompt", "image"]) |
| expected_workflow_blocks=FLUX_IMAGE2IMAGE_WORKFLOWS |
| |
| defget_pipeline(self, components_manager=None, torch_dtype=torch.float32): |
| pipeline=super().get_pipeline(components_manager, torch_dtype) |
| |
| # Override `vae_scale_factor` here as currently, `image_processor` is initialized with |
| # fixed constants instead of |
| # https://github.com/huggingface/diffusers/blob/d54622c2679d700b425ad61abce9b80fc36212c0/src/diffusers/pipelines/flux/pipeline_flux_img2img.py#L230C9-L232C10 |
| pipeline.image_processor=VaeImageProcessor(vae_scale_factor=2) |
| returnpipeline |
| |
| defget_dummy_inputs(self, seed=0): |
| generator=self.get_generator(seed) |
| inputs= { |
| "prompt": "A painting of a squirrel eating a burger", |
| "generator": generator, |
| "num_inference_steps": 4, |
| "guidance_scale": 5.0, |
| "height": 8, |
| "width": 8, |
| "max_sequence_length": 48, |
| "output_type": "pt", |
| } |
| image=floats_tensor((1, 3, 32, 32), rng=random.Random(seed)).to(torch_device) |
| image=image.cpu().permute(0, 2, 3, 1)[0] |
| init_image=PIL.Image.fromarray(np.uint8(image)).convert("RGB") |
| |
| inputs["image"] =init_image |
| inputs["strength"] =0.5 |
| |
| returninputs |
| |
| deftest_save_from_pretrained(self, tmp_path): |
| pipes= [] |
| base_pipe=self.get_pipeline().to(torch_device) |
| pipes.append(base_pipe) |
| |
| base_pipe.save_pretrained(str(tmp_path)) |
| pipe=ModularPipeline.from_pretrained(tmp_path).to(torch_device) |
| pipe.load_components(torch_dtype=torch.float32) |
| pipe.to(torch_device) |
| pipe.image_processor=VaeImageProcessor(vae_scale_factor=2) |
| |
| pipes.append(pipe) |
| |
| image_slices= [] |
| forpipeinpipes: |
| inputs=self.get_dummy_inputs() |
| image=pipe(**inputs, output="images") |
| |
| image_slices.append(image[0, -3:, -3:, -1].flatten()) |
| |
| asserttorch.abs(image_slices[0] -image_slices[1]).max() <1e-3 |
| |
| deftest_float16_inference(self): |
| super().test_float16_inference(8e-2) |
| |
| |
| FLUX_KONTEXT_WORKFLOWS= { |
| "text2image": [ |
| ("text_encoder", "FluxTextEncoderStep"), |
| ("denoise.input", "FluxTextInputStep"), |
| ("denoise.before_denoise.prepare_latents", "FluxPrepareLatentsStep"), |
| ("denoise.before_denoise.set_timesteps", "FluxSetTimestepsStep"), |
| ("denoise.before_denoise.prepare_rope_inputs", "FluxRoPEInputsStep"), |
| ("denoise.denoise", "FluxKontextDenoiseStep"), |
| ("decode", "FluxDecodeStep"), |
| ], |
| "image_conditioned": [ |
| ("text_encoder", "FluxTextEncoderStep"), |
| ("vae_encoder.preprocess", "FluxKontextProcessImagesInputStep"), |
| ("vae_encoder.encode", "FluxVaeEncoderStep"), |
| ("denoise.input.set_resolution", "FluxKontextSetResolutionStep"), |
| ("denoise.input.text_inputs", "FluxTextInputStep"), |
| ("denoise.input.additional_inputs", "FluxKontextAdditionalInputsStep"), |
| ("denoise.before_denoise.prepare_latents", "FluxPrepareLatentsStep"), |
| ("denoise.before_denoise.set_timesteps", "FluxSetTimestepsStep"), |
| ("denoise.before_denoise.prepare_rope_inputs", "FluxKontextRoPEInputsStep"), |
| ("denoise.denoise", "FluxKontextDenoiseStep"), |
| ("decode", "FluxDecodeStep"), |
| ], |
| } |
| |
| |
| classTestFluxKontextModularPipelineFast(ModularPipelineTesterMixin): |
Problem:
Fast tests exist for these variants, but slow/nightly coverage is missing for Flux img2img, inpaint, fill, control, control img2img, control inpaint, Kontext, Kontext inpaint, ControlNet img2img, ControlNet inpaint, and Flux modular pipelines.
Impact:
Several public Flux variants are not covered against real checkpoints, schedulers, tokenizer/text-encoder stacks, or pipeline loading paths. This is especially risky for Flux because many bugs only appear with real component shapes, offload behavior, attention processors, and true CFG paths.
Reproduction:
importastfrompathlibimportPathfiles=sorted(Path("tests").glob("**/*flux*.py"))
forpathinfiles:
tree=ast.parse(path.read_text())
slow_classes= []
fornodeintree.body:
ifisinstance(node, ast.ClassDef):
decorators= {getattr(d, "id", getattr(d, "attr", "")) fordinnode.decorator_list}
ifdecorators& {"slow", "nightly"}:
slow_classes.append(node.name)
ifpath.match("tests/pipelines/flux/*") orpath.match("tests/pipelines/controlnet_flux/*") orpath.match("tests/modular_pipelines/flux/*"):
print(path, slow_classes)Relevant precedent:
Existing real-checkpoint Flux slow coverage is present for base Flux, Flux Redux, and base Flux ControlNet:
| @nightly |
| @require_big_accelerator |
| classFluxPipelineSlowTests(unittest.TestCase): |
| pipeline_class=FluxPipeline |
| repo_id="black-forest-labs/FLUX.1-schnell" |
| |
| defsetUp(self): |
| super().setUp() |
| gc.collect() |
| backend_empty_cache(torch_device) |
| |
| deftearDown(self): |
| super().tearDown() |
| gc.collect() |
| backend_empty_cache(torch_device) |
| |
| defget_inputs(self, device, seed=0): |
| generator=torch.Generator(device="cpu").manual_seed(seed) |
| |
| prompt_embeds=torch.load( |
| hf_hub_download(repo_id="diffusers/test-slices", repo_type="dataset", filename="flux/prompt_embeds.pt") |
| ).to(torch_device) |
| pooled_prompt_embeds=torch.load( |
| hf_hub_download( |
| repo_id="diffusers/test-slices", repo_type="dataset", filename="flux/pooled_prompt_embeds.pt" |
| ) |
| ).to(torch_device) |
| return { |
| "prompt_embeds": prompt_embeds, |
| "pooled_prompt_embeds": pooled_prompt_embeds, |
| "num_inference_steps": 2, |
| "guidance_scale": 0.0, |
| "max_sequence_length": 256, |
| "output_type": "np", |
| "generator": generator, |
| } |
| |
| deftest_flux_inference(self): |
| pipe=self.pipeline_class.from_pretrained( |
| self.repo_id, torch_dtype=torch.bfloat16, text_encoder=None, text_encoder_2=None |
| ).to(torch_device) |
| |
| inputs=self.get_inputs(torch_device) |
| |
| image=pipe(**inputs).images[0] |
| image_slice=image[0, :10, :10] |
| # fmt: off |
| |
| expected_slices=Expectations( |
| { |
| ("cuda", None): np.array([0.3242, 0.3203, 0.3164, 0.3164, 0.3125, 0.3125, 0.3281, 0.3242, 0.3203, 0.3301, 0.3262, 0.3242, 0.3281, 0.3242, 0.3203, 0.3262, 0.3262, 0.3164, 0.3262, 0.3281, 0.3184, 0.3281, 0.3281, 0.3203, 0.3281, 0.3281, 0.3164, 0.3320, 0.3320, 0.3203], dtype=np.float32,), |
| ("xpu", 3): np.array([0.3301, 0.3281, 0.3359, 0.3203, 0.3203, 0.3281, 0.3281, 0.3301, 0.3340, 0.3281, 0.3320, 0.3359, 0.3281, 0.3301, 0.3320, 0.3242, 0.3301, 0.3281, 0.3242, 0.3320, 0.3320, 0.3281, 0.3320, 0.3320, 0.3262, 0.3320, 0.3301, 0.3301, 0.3359, 0.3320], dtype=np.float32,), |
| } |
| ) |
| expected_slice=expected_slices.get_expectation() |
| # fmt: on |
| |
| max_diff=numpy_cosine_similarity_distance(expected_slice.flatten(), image_slice.flatten()) |
| self.assertLess( |
| max_diff, 1e-4, f"Image slice is different from expected slice: {image_slice} != {expected_slice}" |
| ) |
| |
| |
| @slow |
| @require_big_accelerator |
| classFluxIPAdapterPipelineSlowTests(unittest.TestCase): |
| @slow |
| @require_big_accelerator |
| classFluxReduxSlowTests(unittest.TestCase): |
| @nightly |
| @require_big_accelerator |
| classFluxControlNetPipelineSlowTests(unittest.TestCase): |
Suggested fix:
Add at least one @slow or @nightly smoke test per missing public variant using a small deterministic inference path and real public checkpoint components where available. For modular Flux, add slow parity coverage against the corresponding classic Flux pipeline for text-to-image, image-to-image, and Kontext paths.
fluxmodel/pipeline reviewCommit tested:
0f1abc4ae8b0eb2a3b40e82a310507281144c423Review performed against the repository review rules.
Duplicate search status: searched current GitHub Issues and PRs for
flux, affected class/function names, and failure modes. No likely duplicates found. Broaderghsearches hit rate limits after targeted searches, so remaining modular-pipeline searches were checked through the GitHub connector; related PRs such as#12272and#13482are not duplicates.Issue 1: Flux IP-Adapter masks are accepted but ignored
Affected code:
diffusers/src/diffusers/models/transformers/transformer_flux.py
Lines 183 to 268 in 0f1abc4
diffusers/src/diffusers/models/transformers/transformer_flux.py
Lines 345 to 346 in 0f1abc4
Problem:
FluxIPAdapterAttnProcessor.__call__acceptsip_adapter_masks, andFluxAttention.forwardexplicitly quiets that kwarg, but the processor never reads or applies the masks.Impact:
Region-specific IP-Adapter conditioning silently has no effect for Flux. Users can pass masks without warnings, making masked IP-Adapter results misleading.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/attention_processor.py
Lines 4309 to 4378 in 0f1abc4
Suggested fix:
Apply the same validation/downsampling pattern used by
IPAdapterAttnProcessor2_0, adapted for Flux sequence-shaped hidden states. If mask support is not intended, removeip_adapter_masksfrom the quieted kwargs so users get a warning.Issue 2:
negative_prompt_embedsdo not enable true CFG in several Flux pipelinesAffected code:
diffusers/src/diffusers/pipelines/flux/pipeline_flux_img2img.py
Line 902 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_inpaint.py
Line 977 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_controlnet.py
Line 863 in 0f1abc4
Problem:
These pipelines compute true CFG only with
negative_prompt is not None. They validate and acceptnegative_prompt_embeds/negative_pooled_prompt_embeds, but embeddings alone do not activate true CFG.Impact:
Advanced users passing precomputed negative embeddings get silently different behavior from text negative prompts.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/flux/pipeline_flux.py
Lines 820 to 823 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_kontext.py
Lines 946 to 949 in 0f1abc4
Suggested fix:
Issue 3: Base Flux and Flux Kontext accept mismatched negative prompt embeddings
Affected code:
diffusers/src/diffusers/pipelines/flux/pipeline_flux.py
Lines 483 to 500 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_kontext.py
Lines 534 to 551 in 0f1abc4
Problem:
FluxPipelineandFluxKontextPipelinecheck that negative pooled embeddings are provided with negative prompt embeddings, but they do not verify that negative and positive embedding shapes match.Impact:
Shape or batch mismatches are accepted at input validation time and fail later, or worse, produce hard-to-debug true CFG behavior.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/flux/pipeline_flux_img2img.py
Lines 550 to 556 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_kontext_inpaint.py
Lines 598 to 604 in 0f1abc4
Suggested fix:
Issue 4: Flux ControlNet img2img validates dimensions with wrong operator precedence
Affected code:
diffusers/src/diffusers/pipelines/flux/pipeline_flux_controlnet_image_to_image.py
Lines 442 to 459 in 0f1abc4
Problem:
The dimension check uses
height % self.vae_scale_factor * 2, which is parsed as(height % self.vae_scale_factor) * 2, notheight % (self.vae_scale_factor * 2).Impact:
Invalid dimensions such as
72x72pass validation whenvae_scale_factor == 8, even though Flux latent packing expects dimensions divisible by16.Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/flux/pipeline_flux_img2img.py
Lines 489 to 510 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_controlnet.py
Lines 473 to 489 in 0f1abc4
Suggested fix:
Issue 5: Flux Prior Redux misses tensor-batch and pooled-scale validation
Affected code:
diffusers/src/diffusers/pipelines/flux/pipeline_flux_prior_redux.py
Lines 143 to 179 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_prior_redux.py
Lines 425 to 435 in 0f1abc4
diffusers/src/diffusers/pipelines/flux/pipeline_flux_prior_redux.py
Lines 475 to 478 in 0f1abc4
Problem:
check_inputsonly checks image batch size whenimageis a list. Tensor image batches are skipped. It also checksprompt_embeds_scalelist length but notpooled_prompt_embeds_scale.Impact:
Mismatched image/prompt batches and scale lengths are accepted, then fail later during tensor arithmetic or produce confusing broadcasting behavior.
Reproduction:
Relevant precedent:
Flux text/image pipelines consistently validate paired prompt embedding inputs before denoising, for example:
diffusers/src/diffusers/pipelines/flux/pipeline_flux.py
Lines 483 to 500 in 0f1abc4
Suggested fix:
Determine image batch size for tensors as well as lists, then validate prompt batch size and both scale arguments against that batch size.
Issue 6: Flux modular pipelines import standard Flux pipelines and QwenImage modular helpers
Affected code:
diffusers/src/diffusers/modular_pipelines/flux/before_denoise.py
Line 20 in 0f1abc4
diffusers/src/diffusers/modular_pipelines/flux/before_denoise.py
Line 393 in 0f1abc4
diffusers/src/diffusers/modular_pipelines/flux/before_denoise.py
Lines 544 to 609 in 0f1abc4
diffusers/src/diffusers/modular_pipelines/flux/inputs.py
Lines 18 to 24 in 0f1abc4
diffusers/src/diffusers/modular_pipelines/flux/inputs.py
Lines 212 to 274 in 0f1abc4
diffusers/src/diffusers/modular_pipelines/flux/encoders.py
Lines 171 to 173 in 0f1abc4
Problem:
The modular review rules prohibit hidden imports from classic pipelines and cross-family modular imports. Flux modular blocks import
FluxPipelinefor private helper methods and import QwenImage modular input helpers.Impact:
This creates hidden coupling between modular Flux, classic Flux, and QwenImage. Refactors or lazy-loading changes in one family can break another, and modular Flux becomes harder to serialize, test, and maintain independently.
Reproduction:
Relevant precedent:
decoders.pykeeps its Flux latent unpacking helper local instead of importing the classic pipeline:diffusers/src/diffusers/modular_pipelines/flux/decoders.py
Lines 32 to 44 in 0f1abc4
Suggested fix:
Move shared latent packing, latent id, and dimension helpers into Flux modular-local helpers or a neutral utility module that both classic and modular Flux can import without creating pipeline-family coupling. Copy the Kontext resolution table into Flux modular code or move it to a neutral Flux constants module.
Issue 7: Slow coverage is missing for most Flux pipeline variants
Affected code:
diffusers/tests/pipelines/flux/test_pipeline_flux_img2img.py
Line 21 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_inpaint.py
Line 21 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_fill.py
Line 21 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_control.py
Line 14 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_control_img2img.py
Line 22 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_control_inpaint.py
Line 21 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_kontext.py
Line 25 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_kontext_inpaint.py
Line 25 in 0f1abc4
diffusers/tests/pipelines/controlnet_flux/test_controlnet_flux_img2img.py
Line 20 in 0f1abc4
diffusers/tests/pipelines/controlnet_flux/test_controlnet_flux_inpaint.py
Line 24 in 0f1abc4
diffusers/tests/modular_pipelines/flux/test_modular_pipeline_flux.py
Lines 48 to 183 in 0f1abc4
Problem:
Fast tests exist for these variants, but slow/nightly coverage is missing for Flux img2img, inpaint, fill, control, control img2img, control inpaint, Kontext, Kontext inpaint, ControlNet img2img, ControlNet inpaint, and Flux modular pipelines.
Impact:
Several public Flux variants are not covered against real checkpoints, schedulers, tokenizer/text-encoder stacks, or pipeline loading paths. This is especially risky for Flux because many bugs only appear with real component shapes, offload behavior, attention processors, and true CFG paths.
Reproduction:
Relevant precedent:
Existing real-checkpoint Flux slow coverage is present for base Flux, Flux Redux, and base Flux ControlNet:
diffusers/tests/pipelines/flux/test_pipeline_flux.py
Lines 238 to 303 in 0f1abc4
diffusers/tests/pipelines/flux/test_pipeline_flux_redux.py
Lines 20 to 22 in 0f1abc4
diffusers/tests/pipelines/controlnet_flux/test_controlnet_flux.py
Lines 212 to 214 in 0f1abc4
Suggested fix:
Add at least one
@slowor@nightlysmoke test per missing public variant using a small deterministic inference path and real public checkpoint components where available. For modular Flux, add slow parity coverage against the corresponding classic Flux pipeline for text-to-image, image-to-image, and Kontext paths.