Skip to content

ovis_image model/pipeline review #13630

Description

@hlky

ovis_image model/pipeline review

Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423

Review performed against the repository review rules. AGENTS.md is referenced by .ai/review-rules.md but is not present in this checkout; the other referenced rule files were read and applied.

Files/categories reviewed: target pipeline/model files, public imports and lazy loading, config/serialization, dtype/device/offload paths, attention processor behavior, docs, and fast/slow test coverage.

Duplicate search status: gh search hit the GitHub API rate limit, so I checked GitHub web issue/PR searches for OvisImage, ovis_image, OvisImageTransformer2DModel AttentionMixin, and OvisImagePipeline num_images_per_prompt. I did not find an exact Ovis duplicate. Related but not duplicate: #12186 covers the same missing-AttentionMixin pattern for WanVACETransformer3DModel.

Issue 1: Transformer does not expose attention processor APIs

Affected code:

from ...configuration_utilsimportConfigMixin, register_to_config
from ...loadersimportFromOriginalModelMixin, PeftAdapterMixin
from ...utilsimportlogging
from ...utils.torch_utilsimportmaybe_allow_in_graph
from ..attentionimportAttentionModuleMixin, FeedForward
from ..attention_dispatchimportdispatch_attention_fn
from ..cache_utilsimportCacheMixin

classOvisImageTransformer2DModel(
ModelMixin,
ConfigMixin,
PeftAdapterMixin,
FromOriginalModelMixin,
CacheMixin,
):

Problem:
OvisImageTransformer2DModel defines OvisImageAttention modules but does not inherit AttentionMixin. That leaves the model without the standard attn_processors, set_attn_processor, fuse_qkv_projections, and unfuse_qkv_projections APIs expected by related transformer families.

Impact:
Users and tests cannot swap attention processors, inspect processors, or use QKV fusion through the model-level API.

Reproduction:

fromdiffusersimportOvisImageTransformer2DModelprint(hasattr(OvisImageTransformer2DModel, "set_attn_processor"))
print(hasattr(OvisImageTransformer2DModel, "fuse_qkv_projections"))
asserthasattr(OvisImageTransformer2DModel, "set_attn_processor")

Relevant precedent:

from ..attentionimportAttentionMixin, AttentionModuleMixin, FeedForward
from ..attention_dispatchimportdispatch_attention_fn
from ..cache_utilsimportCacheMixin
from ..embeddingsimport (
CombinedTimestepGuidanceTextProjEmbeddings,
CombinedTimestepTextProjEmbeddings,

classFluxTransformer2DModel(
ModelMixin,
ConfigMixin,
PeftAdapterMixin,
FromOriginalModelMixin,
FluxTransformer2DLoadersMixin,
CacheMixin,
AttentionMixin,
):

from ..attentionimportAttentionMixin, FeedForward

Suggested fix:

from ..attentionimportAttentionMixin, AttentionModuleMixin, FeedForwardclassOvisImageTransformer2DModel(
ModelMixin,
ConfigMixin,
PeftAdapterMixin,
FromOriginalModelMixin,
CacheMixin,
AttentionMixin,
):
...

Issue 2: joint_attention_kwargs is accepted but never reaches attention

Affected code:

self._joint_attention_kwargs=joint_attention_kwargs
self._current_timestep=None
self._interrupt=False
# 2. Define call parameters
ifpromptisnotNoneandisinstance(prompt, str):
batch_size=1
elifpromptisnotNoneandisinstance(prompt, list):
batch_size=len(prompt)
else:
batch_size=prompt_embeds.shape[0]
device=self._execution_device
do_classifier_free_guidance=guidance_scale>1
(
prompt_embeds,
text_ids,
) =self.encode_prompt(
prompt=prompt,
prompt_embeds=prompt_embeds,
device=device,
num_images_per_prompt=num_images_per_prompt,
)
ifdo_classifier_free_guidance:
(
negative_prompt_embeds,
negative_text_ids,
) =self.encode_prompt(
prompt=negative_prompt,
prompt_embeds=negative_prompt_embeds,
device=device,
num_images_per_prompt=num_images_per_prompt,
)
# 4. Prepare latent variables
num_channels_latents=self.transformer.config.in_channels//4
latents, latent_image_ids=self.prepare_latents(
batch_size*num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds.dtype,
device,
generator,
latents,
)
# 5. Prepare timesteps
sigmas=np.linspace(1.0, 1/num_inference_steps, num_inference_steps) ifsigmasisNoneelsesigmas
ifhasattr(self.scheduler.config, "use_flow_sigmas") andself.scheduler.config.use_flow_sigmas:
sigmas=None
image_seq_len=latents.shape[1]
mu=calculate_shift(
image_seq_len,
self.scheduler.config.get("base_image_seq_len", 256),
self.scheduler.config.get("max_image_seq_len", 4096),
self.scheduler.config.get("base_shift", 0.5),
self.scheduler.config.get("max_shift", 1.15),
)
timesteps, num_inference_steps=retrieve_timesteps(
self.scheduler,
num_inference_steps,
device,
sigmas=sigmas,
mu=mu,
)
num_warmup_steps=max(len(timesteps) -num_inference_steps*self.scheduler.order, 0)
self._num_timesteps=len(timesteps)
ifself.joint_attention_kwargsisNone:
self._joint_attention_kwargs= {}

withself.transformer.cache_context("cond"):
noise_pred=self.transformer(
hidden_states=latents,
timestep=timestep/1000,
encoder_hidden_states=prompt_embeds,
txt_ids=text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]
ifdo_classifier_free_guidance:
withself.transformer.cache_context("uncond"):
neg_noise_pred=self.transformer(
hidden_states=latents,
timestep=timestep/1000,
encoder_hidden_states=negative_prompt_embeds,
txt_ids=negative_text_ids,
img_ids=latent_image_ids,
return_dict=False,
)[0]

defforward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor=None,
timestep: torch.LongTensor=None,
img_ids: torch.Tensor=None,
txt_ids: torch.Tensor=None,
return_dict: bool=True,
) ->torch.Tensor|Transformer2DModelOutput:

forindex_block, blockinenumerate(self.transformer_blocks):
iftorch.is_grad_enabled() andself.gradient_checkpointing:
encoder_hidden_states, hidden_states=self._gradient_checkpointing_func(
block,
hidden_states,
encoder_hidden_states,
temb,
image_rotary_emb,
)
else:
encoder_hidden_states, hidden_states=block(
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
temb=temb,
image_rotary_emb=image_rotary_emb,
)
forindex_block, blockinenumerate(self.single_transformer_blocks):
iftorch.is_grad_enabled() andself.gradient_checkpointing:
encoder_hidden_states, hidden_states=self._gradient_checkpointing_func(
block,
hidden_states,
encoder_hidden_states,
temb,
image_rotary_emb,
)
else:
encoder_hidden_states, hidden_states=block(
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
temb=temb,
image_rotary_emb=image_rotary_emb,
)

Problem:
The pipeline exposes joint_attention_kwargs, but OvisImageTransformer2DModel.forward() does not accept it, and the pipeline transformer calls do not pass it. The block classes already accept joint_attention_kwargs, so the plumbing is incomplete.

Impact:
Any user-provided attention kwargs are silently ignored by the pipeline. Direct model calls with the same argument fail.

Reproduction:

importinspectfromdiffusersimportOvisImagePipeline, OvisImageTransformer2DModelprint("pipeline:", "joint_attention_kwargs"ininspect.signature(OvisImagePipeline.__call__).parameters)
print("model:", "joint_attention_kwargs"ininspect.signature(OvisImageTransformer2DModel.forward).parameters)
assert"joint_attention_kwargs"ininspect.signature(OvisImageTransformer2DModel.forward).parameters

Relevant precedent:

joint_attention_kwargs: dict[str, Any] |None=None,
controlnet_block_samples=None,

joint_attention_kwargs,
)
else:
encoder_hidden_states, hidden_states=block(
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
temb=temb,
image_rotary_emb=image_rotary_emb,
joint_attention_kwargs=joint_attention_kwargs,

Suggested fix:

defforward(..., joint_attention_kwargs: dict[str, Any] |None=None, return_dict: bool=True):
...
encoder_hidden_states, hidden_states=block(
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
temb=temb,
image_rotary_emb=image_rotary_emb,
joint_attention_kwargs=joint_attention_kwargs,
)

Also pass joint_attention_kwargs=self.joint_attention_kwargs in both pipeline transformer calls.

Issue 3: guidance_scale property is never initialized

Affected code:

defguidance_scale(self):
returnself._guidance_scale

self._joint_attention_kwargs=joint_attention_kwargs
self._current_timestep=None
self._interrupt=False

Problem:
OvisImagePipeline.guidance_scale returns self._guidance_scale, but __call__ never assigns self._guidance_scale = guidance_scale.

Impact:
Callbacks or downstream code that read pipe.guidance_scale during generation can hit an AttributeError or stale state, unlike related pipelines.

Reproduction:

importinspectfromdiffusersimportOvisImagePipelinesource=inspect.getsource(OvisImagePipeline.__call__)
print("self._guidance_scale = guidance_scale"insource)
assert"self._guidance_scale = guidance_scale"insource

Relevant precedent:

self._guidance_scale=guidance_scale
self._joint_attention_kwargs=joint_attention_kwargs
self._current_timestep=None

self._guidance_scale=guidance_scale
self._attention_kwargs=attention_kwargs
self._current_timestep=None

Suggested fix:

self._guidance_scale=guidance_scaleself._joint_attention_kwargs=joint_attention_kwargsself._current_timestep=Noneself._interrupt=False

Issue 4: Batched prompts break with default negative prompt under CFG

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."
)

# 2. Define call parameters
ifpromptisnotNoneandisinstance(prompt, str):
batch_size=1
elifpromptisnotNoneandisinstance(prompt, list):
batch_size=len(prompt)
else:
batch_size=prompt_embeds.shape[0]
device=self._execution_device
do_classifier_free_guidance=guidance_scale>1
(
prompt_embeds,
text_ids,
) =self.encode_prompt(
prompt=prompt,
prompt_embeds=prompt_embeds,
device=device,
num_images_per_prompt=num_images_per_prompt,
)
ifdo_classifier_free_guidance:
(
negative_prompt_embeds,
negative_text_ids,
) =self.encode_prompt(
prompt=negative_prompt,
prompt_embeds=negative_prompt_embeds,
device=device,
num_images_per_prompt=num_images_per_prompt,
)

Problem:
With prompt as a list and default negative_prompt="", positive embeddings are batched to len(prompt), but negative embeddings are encoded as batch size 1. CFG then calls the transformer with mismatched latent and negative prompt batch sizes.

Impact:
The default CFG path fails for normal batched text-to-image usage unless users manually pass a negative prompt list of matching length.

Reproduction:

importtorchfromdiffusersimportOvisImagePipelinepipe=OvisImagePipeline.__new__(OvisImagePipeline)
pipe.text_encoder=type("E", (), {"dtype": torch.float32})()
pipe.transformer=type("T", (), {"dtype": torch.float32})()
deffake_get_ovis_prompt_embeds(prompt, num_images_per_prompt=1, device=None, dtype=None):
prompt= [prompt] ifisinstance(prompt, str) elsepromptreturntorch.zeros(len(prompt) *num_images_per_prompt, 4, 8)
pipe._get_ovis_prompt_embeds=fake_get_ovis_prompt_embedspos, _=pipe.encode_prompt(["cat", "dog"], device=torch.device("cpu"))
neg, _=pipe.encode_prompt("", device=torch.device("cpu"))
print(pos.shape[0], neg.shape[0])
assertpos.shape[0] ==neg.shape[0]

Relevant precedent:

ifdo_classifier_free_guidanceandnegative_prompt_embedsisNone:
negative_prompt=negative_promptor""
uncond_tokens= [negative_prompt] *batch_sizeifisinstance(negative_prompt, str) elsenegative_prompt
max_length=prompt_embeds.shape[1]
uncond_input=self.tokenizer(
uncond_tokens,
truncation=True,
max_length=max_length,
padding="max_length",
return_tensors="pt",
)
uncond_input= {k: v.to(device) fork, vinuncond_input.items()}
negative_prompt_embeds=self.text_encoder(**uncond_input)[0]
negative_prompt_attention_mask= (
uncond_input["attention_mask"].unsqueeze(-1).expand(negative_prompt_embeds.shape)
)
negative_prompt_embeds=negative_prompt_embeds*negative_prompt_attention_mask
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)

Suggested fix:

ifdo_classifier_free_guidanceandnegative_prompt_embedsisNone:
ifnegative_promptisNone:
negative_prompt=""ifisinstance(negative_prompt, str):
negative_prompt= [negative_prompt] *batch_sizeeliflen(negative_prompt) !=batch_size:
raiseValueError(
f"`negative_prompt` has batch size {len(negative_prompt)}, but `prompt` has batch size {batch_size}."
)

Issue 5: Precomputed prompt_embeds are not moved or repeated

Affected code:

defencode_prompt(
self,
prompt: str|list[str],
device: torch.device|None=None,
num_images_per_prompt: int=1,
prompt_embeds: torch.FloatTensor|None=None,
):
r"""
Args:
prompt (`str`, *optional*):
prompt to be encoded
device: (`torch.device`):
torch device
num_images_per_prompt (`int`):
number of images that should be generated per prompt
prompt_embeds (`torch.FloatTensor`, *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.
"""
device=deviceorself._execution_device
ifprompt_embedsisNone:
prompt_embeds=self._get_ovis_prompt_embeds(
prompt=prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
)
dtype=self.text_encoder.dtypeifself.text_encoderisnotNoneelseself.transformer.dtype
text_ids=torch.zeros(prompt_embeds.shape[1], 3)
text_ids[..., 1] =text_ids[..., 1] +torch.arange(prompt_embeds.shape[1])[None, :]
text_ids[..., 2] =text_ids[..., 2] +torch.arange(prompt_embeds.shape[1])[None, :]
text_ids=text_ids.to(device=device, dtype=dtype)
returnprompt_embeds, text_ids

# 4. Prepare latent variables
num_channels_latents=self.transformer.config.in_channels//4
latents, latent_image_ids=self.prepare_latents(
batch_size*num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds.dtype,
device,
generator,
latents,
)

Problem:
When prompt_embeds is supplied, encode_prompt() does not move it to the execution device/dtype and does not repeat it for num_images_per_prompt. The pipeline still prepares latents for batch_size * num_images_per_prompt.

Impact:
Precomputed embeddings can fail with device mismatches on GPU/offload paths and batch mismatches when generating multiple images per prompt.

Reproduction:

importtorchfromdiffusersimportOvisImagePipelinepipe=OvisImagePipeline.__new__(OvisImagePipeline)
pipe.text_encoder=Nonepipe.transformer=type("T", (), {"dtype": torch.float16})()
embeds=torch.randn(1, 4, 8, dtype=torch.float32)
out, ids=pipe.encode_prompt(None, device=torch.device("meta"), prompt_embeds=embeds, num_images_per_prompt=2)
print(out.shape, out.device, ids.device)
assertout.shape[0] ==2assertout.device==ids.device

Relevant precedent:

prompt= [prompt] ifisinstance(prompt, str) elseprompt
batch_size=len(prompt) ifprompt_embedsisNoneelseprompt_embeds.shape[0]
ifprompt_embedsisNone:
prompt_embeds, prompt_embeds_mask=self._get_qwen_prompt_embeds(prompt, device)
prompt_embeds=prompt_embeds[:, :max_sequence_length]
_, seq_len, _=prompt_embeds.shape
prompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds=prompt_embeds.view(batch_size*num_images_per_prompt, seq_len, -1)
ifprompt_embeds_maskisnotNone:
prompt_embeds_mask=prompt_embeds_mask[:, :max_sequence_length]
prompt_embeds_mask=prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
prompt_embeds_mask=prompt_embeds_mask.view(batch_size*num_images_per_prompt, seq_len)

Suggested fix:

batch_size=len(prompt) ifprompt_embedsisNoneelseprompt_embeds.shape[0]
ifprompt_embedsisNone:
prompt_embeds=self._get_ovis_prompt_embeds(...)
dtype=self.text_encoder.dtypeifself.text_encoderisnotNoneelseself.transformer.dtypeprompt_embeds=prompt_embeds.to(device=device, dtype=dtype)
_, seq_len, _=prompt_embeds.shapeprompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds=prompt_embeds.view(batch_size*num_images_per_prompt, seq_len, -1)

Issue 6: max_sequence_length is validated but ignored

Affected code:

def_get_ovis_prompt_embeds(
self,
prompt: str|list[str] =None,
num_images_per_prompt: int=1,
device: torch.device|None=None,
dtype: torch.dtype|None=None,
):
device=deviceorself._execution_device
dtype=dtypeorself.text_encoder.dtype
messages=self._get_messages(prompt)
batch_size=len(messages)
tokens=self.tokenizer(
messages,
padding="max_length",
truncation=True,
max_length=self.tokenizer_max_length,
return_tensors="pt",
add_special_tokens=False,
)
input_ids=tokens.input_ids.to(device)
attention_mask=tokens.attention_mask.to(device)
outputs=self.text_encoder(
input_ids=input_ids,
attention_mask=attention_mask,
)
prompt_embeds=outputs.last_hidden_state
prompt_embeds=prompt_embeds*attention_mask[..., None]

defencode_prompt(
self,
prompt: str|list[str],
device: torch.device|None=None,
num_images_per_prompt: int=1,
prompt_embeds: torch.FloatTensor|None=None,
):
r"""
Args:
prompt (`str`, *optional*):
prompt to be encoded
device: (`torch.device`):
torch device
num_images_per_prompt (`int`):
number of images that should be generated per prompt
prompt_embeds (`torch.FloatTensor`, *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.
"""
device=deviceorself._execution_device
ifprompt_embedsisNone:
prompt_embeds=self._get_ovis_prompt_embeds(
prompt=prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
)
dtype=self.text_encoder.dtypeifself.text_encoderisnotNoneelseself.transformer.dtype
text_ids=torch.zeros(prompt_embeds.shape[1], 3)
text_ids[..., 1] =text_ids[..., 1] +torch.arange(prompt_embeds.shape[1])[None, :]
text_ids[..., 2] =text_ids[..., 2] +torch.arange(prompt_embeds.shape[1])[None, :]
text_ids=text_ids.to(device=device, dtype=dtype)
returnprompt_embeds, text_ids

callback_on_step_end_tensor_inputs: list[str] = ["latents"],
max_sequence_length: int=256,
):
r"""
Function invoked when calling the pipeline for generation.
Args:
prompt (`str` or `list[str]`, *optional*):
The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
instead.
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. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
not greater than `1`).
guidance_scale (`float`, *optional*, defaults to 1.0):
True classifier-free guidance (guidance scale) is enabled when `guidance_scale` > 1 and
`negative_prompt` is provided.
height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
The height in pixels of the generated image. This is set to 1024 by default for the best results.
width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
The width in pixels of the generated image. This is set to 1024 by default for the best results.
num_inference_steps (`int`, *optional*, defaults to 50):
The number of denoising steps. More denoising steps usually lead to a higher quality image at the
expense of slower inference.
sigmas (`list[float]`, *optional*):
Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
will be used.
num_images_per_prompt (`int`, *optional*, defaults to 1):
The number of images to generate per prompt.
generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
to make generation deterministic.
latents (`torch.FloatTensor`, *optional*):
Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image
generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
tensor will be generated by sampling using the supplied random `generator`.
prompt_embeds (`torch.FloatTensor`, *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.FloatTensor`, *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.
output_type (`str`, *optional*, defaults to `"pil"`):
The output format of the generate image. Choose between
[PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~pipelines.flux.FluxPipelineOutput`] instead of a plain tuple.
joint_attention_kwargs (`dict`, *optional*):
A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
`self.processor` in
[diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
callback_on_step_end (`Callable`, *optional*):
A function that calls at the end of each denoising steps during the inference. The function is called
with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int,
callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by
`callback_on_step_end_tensor_inputs`.
callback_on_step_end_tensor_inputs (`List`, *optional*):
The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
`._callback_tensor_inputs` attribute of your pipeline class.
max_sequence_length (`int` defaults to 512): Maximum sequence length to use with the `prompt`.
Examples:
Returns:
[`~pipelines.ovis_image.OvisImagePipelineOutput`] or `tuple`:
[`~pipelines.ovis_image.OvisImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple`. When
returning a tuple, the first element is a list with the generated images.
"""
height=heightorself.default_sample_size*self.vae_scale_factor
width=widthorself.default_sample_size*self.vae_scale_factor
# 1. Check inputs. Raise error if not correct
self.check_inputs(
prompt,
height,
width,
negative_prompt=negative_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
max_sequence_length=max_sequence_length,
)
self._joint_attention_kwargs=joint_attention_kwargs
self._current_timestep=None
self._interrupt=False
# 2. Define call parameters
ifpromptisnotNoneandisinstance(prompt, str):
batch_size=1
elifpromptisnotNoneandisinstance(prompt, list):
batch_size=len(prompt)
else:
batch_size=prompt_embeds.shape[0]
device=self._execution_device
do_classifier_free_guidance=guidance_scale>1
(
prompt_embeds,
text_ids,
) =self.encode_prompt(
prompt=prompt,
prompt_embeds=prompt_embeds,
device=device,
num_images_per_prompt=num_images_per_prompt,
)
ifdo_classifier_free_guidance:
(
negative_prompt_embeds,
negative_text_ids,
) =self.encode_prompt(
prompt=negative_prompt,
prompt_embeds=negative_prompt_embeds,
device=device,
num_images_per_prompt=num_images_per_prompt,
)

Problem:
__call__ accepts and validates max_sequence_length, but encode_prompt() has no such parameter and _get_ovis_prompt_embeds() always tokenizes with self.tokenizer_max_length.

Impact:
Users cannot reduce prompt sequence length for speed/memory, and the public argument is misleading.

Reproduction:

importinspectfromdiffusersimportOvisImagePipelineassert"max_sequence_length"ininspect.signature(OvisImagePipeline.__call__).parametersassert"max_sequence_length"ininspect.signature(OvisImagePipeline.encode_prompt).parameters

Relevant precedent:

defencode_prompt(
self,
prompt: str|list[str],
device: torch.device|None=None,
num_images_per_prompt: int=1,
prompt_embeds: torch.Tensor|None=None,
prompt_embeds_mask: torch.Tensor|None=None,
max_sequence_length: int=1024,
):
r"""
Args:
prompt (`str` or `list[str]`, *optional*):
prompt to be encoded
device: (`torch.device`):
torch device
num_images_per_prompt (`int`):
number of images that should be generated per prompt
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.
"""
device=deviceorself._execution_device
prompt= [prompt] ifisinstance(prompt, str) elseprompt
batch_size=len(prompt) ifprompt_embedsisNoneelseprompt_embeds.shape[0]
ifprompt_embedsisNone:
prompt_embeds, prompt_embeds_mask=self._get_qwen_prompt_embeds(prompt, device)
prompt_embeds=prompt_embeds[:, :max_sequence_length]
_, seq_len, _=prompt_embeds.shape
prompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds=prompt_embeds.view(batch_size*num_images_per_prompt, seq_len, -1)
ifprompt_embeds_maskisnotNone:
prompt_embeds_mask=prompt_embeds_mask[:, :max_sequence_length]
prompt_embeds_mask=prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
prompt_embeds_mask=prompt_embeds_mask.view(batch_size*num_images_per_prompt, seq_len)

Suggested fix:

def_get_ovis_prompt_embeds(..., max_sequence_length: int=256):
max_length=max_sequence_length+self.user_prompt_begin_idtokens=self.tokenizer(..., max_length=max_length, ...)
prompt_embeds=prompt_embeds[:, self.user_prompt_begin_id : self.user_prompt_begin_id+max_sequence_length, :]
defencode_prompt(..., max_sequence_length: int=256):
...

Then pass max_sequence_length=max_sequence_length from __call__.

Issue 7: No fast or slow tests cover ovis_image

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/ovis_image/__init__.py#L1

Problem:
tests/pipelines/ovis_image/ only contains an empty __init__.py, and there is no model test for OvisImageTransformer2DModel. No fast or slow tests reference OvisImage or ovis_image.

Impact:
The import surface, prompt batching, callback properties, attention APIs, serialization, and slow checkpoint path can regress without CI coverage.

Reproduction:

frompathlibimportPathpaths= []
forpathinPath("tests").rglob("test*.py"):
text=path.read_text(encoding="utf-8", errors="ignore")
if"OvisImage"intextor"ovis_image"intextor"ovis-image"intext.lower():
paths.append(str(path))
print(paths)
assertpaths, "No fast or slow Ovis tests found"

Relevant precedent:

classQwenImageTransformerTesterConfig(BaseModelTesterConfig):
@property
defmodel_class(self):
returnQwenImageTransformer2DModel
@property
defoutput_shape(self) ->tuple[int, int]:
return (16, 16)
@property
definput_shape(self) ->tuple[int, int]:
return (16, 16)
@property
defmodel_split_percents(self) ->list:
return [0.7, 0.6, 0.6]
@property
defmain_input_name(self) ->str:
return"hidden_states"
@property
defgenerator(self):
returntorch.Generator("cpu").manual_seed(0)
defget_init_dict(self) ->dict[str, int|list[int]]:
return {
"patch_size": 2,
"in_channels": 16,
"out_channels": 4,
"num_layers": 2,
"attention_head_dim": 16,
"num_attention_heads": 4,
"joint_attention_dim": 16,
"guidance_embeds": False,
"axes_dims_rope": (8, 4, 4),
}

classQwenImagePipelineFastTests(PipelineTesterMixin, unittest.TestCase):
pipeline_class=QwenImagePipeline
params=TEXT_TO_IMAGE_PARAMS- {"cross_attention_kwargs"}
batch_params=TEXT_TO_IMAGE_BATCH_PARAMS
image_params=TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params=TEXT_TO_IMAGE_IMAGE_PARAMS
required_optional_params=frozenset(
[
"num_inference_steps",
"generator",
"latents",
"return_dict",
"callback_on_step_end",
"callback_on_step_end_tensor_inputs",
]
)
supports_dduf=False
test_xformers_attention=False
test_layerwise_casting=True
test_group_offloading=True
defget_dummy_components(self):
torch.manual_seed(0)
transformer=QwenImageTransformer2DModel(
patch_size=2,
in_channels=16,
out_channels=4,
num_layers=2,
attention_head_dim=16,
num_attention_heads=3,
joint_attention_dim=16,
guidance_embeds=False,
axes_dims_rope=(8, 4, 4),
)
torch.manual_seed(0)
z_dim=4
vae=AutoencoderKLQwenImage(
base_dim=z_dim*6,
z_dim=z_dim,
dim_mult=[1, 2, 4],
num_res_blocks=1,
temperal_downsample=[False, True],
# fmt: off
latents_mean=[0.0] *4,
latents_std=[1.0] *4,
# fmt: on
)
torch.manual_seed(0)
scheduler=FlowMatchEulerDiscreteScheduler()
torch.manual_seed(0)
config=Qwen2_5_VLConfig(
text_config={
"hidden_size": 16,
"intermediate_size": 16,
"num_hidden_layers": 2,
"num_attention_heads": 2,
"num_key_value_heads": 2,
"rope_scaling": {
"mrope_section": [1, 1, 2],
"rope_type": "default",
"type": "default",
},
"rope_theta": 1000000.0,
},
vision_config={
"depth": 2,
"hidden_size": 16,
"intermediate_size": 16,
"num_heads": 2,
"out_hidden_size": 16,
},
hidden_size=16,
vocab_size=152064,
vision_end_token_id=151653,
vision_start_token_id=151652,
vision_token_id=151654,
)
text_encoder=Qwen2_5_VLForConditionalGeneration(config).eval()
tokenizer=Qwen2Tokenizer.from_pretrained("hf-internal-testing/tiny-random-Qwen2VLForConditionalGeneration")
components= {
"transformer": transformer,

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):

Suggested fix:
Add a model fast test using ModelTesterMixin and AttentionTesterMixin, add a pipeline fast test with tiny synthetic components, and add at least one @slow pipeline smoke test for the published Ovis checkpoint. These tests should cover top-level imports, save/load, attention processor APIs, batched prompts with CFG, prompt_embeds, callback properties, and max_sequence_length.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions