Skip to content

cogview4 model/pipeline review #13621

Description

@hlky

cogview4 model/pipeline review

Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423

Review performed against the repository review rules.

Duplicate-search status: gh search was API-rate-limited, so I used the GitHub connector searches for cogview4, affected classes/files, attention_mask, dispatch_attention_fn, sigmas, callback_on_step_end, CogView4PipelineOutput, CogView4ControlPipeline load_lora_weights, and test coverage. I found no exact duplicates for the findings below. Related: #10962 and #10966 fixed the same prompt/negative embed shape problem for the base pipeline, not the control pipeline. The cache-context part of Issue 3 is already broadly tracked by #12760.

Issue 1: Text attention masks do not actually mask tokens

Affected code:

ifattention_maskisnotNone:
text_attn_mask=attention_mask
asserttext_attn_mask.dim() ==2, "the shape of text_attn_mask should be (batch_size, text_seq_length)"
text_attn_mask=text_attn_mask.float().to(query.device)
mix_attn_mask=torch.ones((batch_size, text_seq_length+image_seq_length), device=query.device)
mix_attn_mask[:, :text_seq_length] =text_attn_mask
mix_attn_mask=mix_attn_mask.unsqueeze(2)
attn_mask_matrix=mix_attn_mask @ mix_attn_mask.transpose(1, 2)
attention_mask= (attn_mask_matrix>0).unsqueeze(1).to(query.dtype)
hidden_states=F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False

withself.transformer.cache_context("cond"):
noise_pred_cond=self.transformer(
hidden_states=latent_model_input,
encoder_hidden_states=prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
# perform guidance
ifself.do_classifier_free_guidance:
withself.transformer.cache_context("uncond"):
noise_pred_uncond=self.transformer(
hidden_states=latent_model_input,
encoder_hidden_states=negative_prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]

hidden_states=latent_model_input,
encoder_hidden_states=prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
# perform guidance
ifself.do_classifier_free_guidance:
noise_pred_uncond=self.transformer(
hidden_states=latent_model_input,
encoder_hidden_states=negative_prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,

attention_mask=tokenizer(
prompts,
padding="longest", # not use max length
max_length=args.max_sequence_length,
truncation=True,
add_special_tokens=True,
return_tensors="pt",
).attention_mask.float()

Problem:
CogView4AttnProcessor converts the text mask to query.dtype before passing it to SDPA. Float SDPA masks are additive bias masks, so 0.0 does not block masked positions. The pipelines also never pass text masks to the transformer, even though _get_glm_embeds pads prompts to a multiple of 16. The training script separately builds an unpadded tokenizer mask, so its mask length can disagree with padded prompt_embeds.

Impact:
Padded or explicitly masked text tokens can affect image tokens. Batched prompts with different lengths can produce different results from single-prompt inference, and training can fail or train with invalid masks.

Reproduction:

importtorchfromdiffusersimportCogView4Transformer2DModeltorch.manual_seed(0)
model=CogView4Transformer2DModel(
patch_size=2, in_channels=4, out_channels=4, num_layers=1,
attention_head_dim=4, num_attention_heads=2, text_embed_dim=8,
time_embed_dim=8, condition_dim=4,
).eval()
hidden_states=torch.randn(1, 4, 8, 8)
encoder_hidden_states=torch.randn(1, 4, 8)
mask=torch.tensor([[0, 1, 1, 1]])
poisoned=encoder_hidden_states.clone()
poisoned[:, 0] =1_000_000.0cleaned=encoder_hidden_states.clone()
cleaned[:, 0] =0.0kwargs=dict(
hidden_states=hidden_states,
timestep=torch.tensor([1]),
original_size=torch.tensor([[64, 64]]),
target_size=torch.tensor([[64, 64]]),
crop_coords=torch.tensor([[0, 0]]),
attention_mask=mask,
return_dict=False,
)
withtorch.no_grad():
out_poisoned=model(encoder_hidden_states=poisoned, **kwargs)[0]
out_cleaned=model(encoder_hidden_states=cleaned, **kwargs)[0]
leak= (out_poisoned-out_cleaned).abs().max().item()
assertleak<1e-6, f"masked token leaked into image output: {leak}"

Relevant precedent:

txt_tokens=self.tokenizer(
txt, max_length=self.tokenizer_max_length+drop_idx, padding=True, truncation=True, return_tensors="pt"
).to(device)
encoder_hidden_states=self.text_encoder(
input_ids=txt_tokens.input_ids,
attention_mask=txt_tokens.attention_mask,
output_hidden_states=True,
)
hidden_states=encoder_hidden_states.hidden_states[-1]
split_hidden_states=self._extract_masked_hidden(hidden_states, txt_tokens.attention_mask)
split_hidden_states= [e[drop_idx:] foreinsplit_hidden_states]
attn_mask_list= [torch.ones(e.size(0), dtype=torch.long, device=e.device) foreinsplit_hidden_states]
max_seq_len=max([e.size(0) foreinsplit_hidden_states])
prompt_embeds=torch.stack(
[torch.cat([u, u.new_zeros(max_seq_len-u.size(0), u.size(1))]) foruinsplit_hidden_states]
)
encoder_attention_mask=torch.stack(
[torch.cat([u, u.new_zeros(max_seq_len-u.size(0))]) foruinattn_mask_list]
)
prompt_embeds=prompt_embeds.to(dtype=dtype, device=device)
returnprompt_embeds, encoder_attention_mask

withself.transformer.cache_context("cond"):
noise_pred=self.transformer(
hidden_states=latents,
timestep=timestep/1000,
guidance=guidance,
encoder_hidden_states_mask=prompt_embeds_mask,
encoder_hidden_states=prompt_embeds,
img_shapes=img_shapes,
attention_kwargs=self.attention_kwargs,
return_dict=False,
)[0]

ifencoder_hidden_states_maskisnotNone:
# Build joint mask: [text_mask, all_ones_for_image]
batch_size, image_seq_len=hidden_states.shape[:2]
image_mask=torch.ones((batch_size, image_seq_len), dtype=torch.bool, device=hidden_states.device)
joint_attention_mask=torch.cat([encoder_hidden_states_mask, image_mask], dim=1)
joint_attention_mask=joint_attention_mask[:, None, None, :]
block_attention_kwargs["attention_mask"] =joint_attention_mask

Suggested fix:

# Keep this as bool. Do not cast to query.dtype.attention_mask= (attn_mask_matrix>0).unsqueeze(1)

Also return padded attention masks from _get_glm_embeds / encode_prompt, repeat them with num_images_per_prompt, and pass the conditional and unconditional masks to self.transformer(...). The training script should pad attention_mask with leading zeros using the same pad_length logic as _get_glm_embeds.

Issue 2: Attention backend selection is a no-op for CogView4

Affected code:

classCogView4AttnProcessor:
"""
Processor for implementing scaled dot-product attention for the CogView4 model. It applies a rotary embedding on
query and key vectors, but does not include spatial normalization.
The processor supports passing an attention mask for text tokens. The attention mask should have shape (batch_size,
text_seq_length) where 1 indicates a non-padded token and 0 indicates a padded token.
"""
def__init__(self):
ifnothasattr(F, "scaled_dot_product_attention"):

hidden_states=F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False

hidden_states=F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False

Problem:
The CogView4 processors call F.scaled_dot_product_attention directly and do not define _attention_backend / _parallel_config. model.set_attention_backend(...) therefore cannot steer CogView4 attention to the requested backend, contrary to the review rules.

Impact:
Users cannot opt into supported diffusers attention backends for CogView4, and context-parallel/backend validation cannot reason about this model correctly.

Reproduction:

fromdiffusersimportCogView4Transformer2DModelmodel=CogView4Transformer2DModel(
patch_size=2, in_channels=4, out_channels=4, num_layers=1,
attention_head_dim=4, num_attention_heads=2, text_embed_dim=8,
time_embed_dim=8, condition_dim=4,
)
model.set_attention_backend("native")
processor=model.transformer_blocks[0].attn1.processorasserthasattr(processor, "_attention_backend"), "CogView4 processor ignores set_attention_backend()"

Relevant precedent:

classFluxAttnProcessor:
_attention_backend=None
_parallel_config=None
def__init__(self):
ifnothasattr(F, "scaled_dot_product_attention"):
raiseImportError(f"{self.__class__.__name__} requires PyTorch 2.0. Please upgrade your pytorch version.")
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,
) ->torch.Tensor:
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)
ifattn.added_kv_proj_dimisnotNone:
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,
backend=self._attention_backend,

classQwenDoubleStreamAttnProcessor2_0:
"""
Attention processor for Qwen double-stream architecture, matching DoubleStreamLayerMegatron logic. This processor
implements joint attention computation where text and image streams are processed together.
"""
_attention_backend=None
_parallel_config=None
def__init__(self):
ifnothasattr(F, "scaled_dot_product_attention"):
raiseImportError(
"QwenDoubleStreamAttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
)
def__call__(
self,
attn: Attention,
hidden_states: torch.FloatTensor, # Image stream
encoder_hidden_states: torch.FloatTensor=None, # Text stream
encoder_hidden_states_mask: torch.FloatTensor=None,
attention_mask: torch.FloatTensor|None=None,
image_rotary_emb: torch.Tensor|None=None,
) ->torch.FloatTensor:
ifencoder_hidden_statesisNone:
raiseValueError("QwenDoubleStreamAttnProcessor2_0 requires encoder_hidden_states (text stream)")
seq_txt=encoder_hidden_states.shape[1]
# Compute QKV for image stream (sample projections)
img_query=attn.to_q(hidden_states)
img_key=attn.to_k(hidden_states)
img_value=attn.to_v(hidden_states)
# Compute QKV for text stream (context projections)
txt_query=attn.add_q_proj(encoder_hidden_states)
txt_key=attn.add_k_proj(encoder_hidden_states)
txt_value=attn.add_v_proj(encoder_hidden_states)
# Reshape for multi-head attention
img_query=img_query.unflatten(-1, (attn.heads, -1))
img_key=img_key.unflatten(-1, (attn.heads, -1))
img_value=img_value.unflatten(-1, (attn.heads, -1))
txt_query=txt_query.unflatten(-1, (attn.heads, -1))
txt_key=txt_key.unflatten(-1, (attn.heads, -1))
txt_value=txt_value.unflatten(-1, (attn.heads, -1))
# Apply QK normalization
ifattn.norm_qisnotNone:
img_query=attn.norm_q(img_query)
ifattn.norm_kisnotNone:
img_key=attn.norm_k(img_key)
ifattn.norm_added_qisnotNone:
txt_query=attn.norm_added_q(txt_query)
ifattn.norm_added_kisnotNone:
txt_key=attn.norm_added_k(txt_key)
# Apply RoPE
ifimage_rotary_embisnotNone:
img_freqs, txt_freqs=image_rotary_emb
img_query=apply_rotary_emb_qwen(img_query, img_freqs, use_real=False)
img_key=apply_rotary_emb_qwen(img_key, img_freqs, use_real=False)
txt_query=apply_rotary_emb_qwen(txt_query, txt_freqs, use_real=False)
txt_key=apply_rotary_emb_qwen(txt_key, txt_freqs, use_real=False)
# Concatenate for joint attention
# Order: [text, image]
joint_query=torch.cat([txt_query, img_query], dim=1)
joint_key=torch.cat([txt_key, img_key], dim=1)
joint_value=torch.cat([txt_value, img_value], dim=1)
joint_hidden_states=dispatch_attention_fn(
joint_query,
joint_key,
joint_value,
attn_mask=attention_mask,
dropout_p=0.0,
is_causal=False,
backend=self._attention_backend,

Suggested fix:

from ..attention_dispatchimportdispatch_attention_fnclassCogView4AttnProcessor:
_attention_backend=None_parallel_config=None
...
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,
)

Apply the same pattern to CogView4TrainingAttnProcessor.

Issue 3: Custom sigmas are unusable

Affected code:

iftimestepsisnotNoneandsigmasisnotNone:
ifnotaccepts_timestepsandnotaccepts_sigmas:
raiseValueError(
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
f" timestep or sigma schedules. Please check whether you are using the correct scheduler."
)
scheduler.set_timesteps(timesteps=timesteps, sigmas=sigmas, device=device, **kwargs)

timesteps= (
np.linspace(self.scheduler.config.num_train_timesteps, 1.0, num_inference_steps)
iftimestepsisNone
elsenp.array(timesteps)
)
timesteps=timesteps.astype(np.int64).astype(np.float32)
sigmas=timesteps/self.scheduler.config.num_train_timestepsifsigmasisNoneelsesigmas
mu=calculate_shift(
image_seq_len,
self.scheduler.config.get("base_image_seq_len", 256),
self.scheduler.config.get("base_shift", 0.25),
self.scheduler.config.get("max_shift", 0.75),
)
ifXLA_AVAILABLE:
timestep_device="cpu"
else:
timestep_device=device
timesteps, num_inference_steps=retrieve_timesteps(
self.scheduler, num_inference_steps, timestep_device, timesteps, sigmas, mu=mu

timesteps= (
np.linspace(self.scheduler.config.num_train_timesteps, 1.0, num_inference_steps)
iftimestepsisNone
elsenp.array(timesteps)
)
timesteps=timesteps.astype(np.int64).astype(np.float32)
sigmas=timesteps/self.scheduler.config.num_train_timestepsifsigmasisNoneelsesigmas
mu=calculate_shift(
image_seq_len,
self.scheduler.config.get("base_image_seq_len", 256),
self.scheduler.config.get("base_shift", 0.25),
self.scheduler.config.get("max_shift", 0.75),
)
ifXLA_AVAILABLE:
timestep_device="cpu"
else:
timestep_device=device
timesteps, num_inference_steps=retrieve_timesteps(
self.scheduler, num_inference_steps, timestep_device, timesteps, sigmas, mu=mu

Problem:
When users pass sigmas, the pipelines still synthesize a default timesteps array of length num_inference_steps and pass both arrays to retrieve_timesteps. A short custom sigma schedule therefore fails before inference.

Impact:
The public sigmas argument is effectively broken unless users also arrange matching timesteps and num_inference_steps, which contradicts the docstring.

Reproduction:

importnumpyasnpfromdiffusersimportFlowMatchEulerDiscreteSchedulerfromdiffusers.pipelines.cogview4.pipeline_cogview4importretrieve_timestepsscheduler=FlowMatchEulerDiscreteScheduler(use_dynamic_shifting=True)
num_inference_steps=50sigmas= [1.0, 0.5]
timesteps=np.linspace(scheduler.config.num_train_timesteps, 1.0, num_inference_steps)
timesteps=timesteps.astype(np.int64).astype(np.float32)
retrieve_timesteps(scheduler, num_inference_steps, "cpu", timesteps, sigmas, mu=0.5)

Relevant precedent:

b=base_shift-m*base_seq_len
mu=image_seq_len*m+b
returnmu
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
defretrieve_timesteps(
scheduler,
num_inference_steps: int|None=None,
device: str|torch.device|None=None,
timesteps: list[int] |None=None,
sigmas: list[float] |None=None,
**kwargs,
):
r"""
Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
Args:
scheduler (`SchedulerMixin`):
The scheduler to get timesteps from.
num_inference_steps (`int`):
The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
must be `None`.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
timesteps (`list[int]`, *optional*):
Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
`num_inference_steps` and `sigmas` must be `None`.
sigmas (`list[float]`, *optional*):
Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
`num_inference_steps` and `timesteps` must be `None`.
Returns:
`tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
second element is the number of inference steps.
"""
iftimestepsisnotNoneandsigmasisnotNone:
raiseValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")

Suggested fix:

iftimestepsisNoneandsigmasisNone:
timesteps=np.linspace(self.scheduler.config.num_train_timesteps, 1.0, num_inference_steps)
timesteps=timesteps.astype(np.int64).astype(np.float32)
eliftimestepsisnotNone:
timesteps=np.array(timesteps).astype(np.int64).astype(np.float32)
ifsigmasisNoneandtimestepsisnotNone:
sigmas=timesteps/self.scheduler.config.num_train_timestepstimesteps, num_inference_steps=retrieve_timesteps(
self.scheduler, num_inference_steps, timestep_device, timesteps, sigmas, mu=mu
)

Issue 4: Step-end callbacks receive sigma instead of timestep

Affected code:

ifcallback_on_step_endisnotNone:
callback_kwargs= {}
forkincallback_on_step_end_tensor_inputs:
callback_kwargs[k] =locals()[k]
callback_outputs=callback_on_step_end(self, i, self.scheduler.sigmas[i], callback_kwargs)

ifcallback_on_step_endisnotNone:
callback_kwargs= {}
forkincallback_on_step_end_tensor_inputs:
callback_kwargs[k] =locals()[k]
callback_outputs=callback_on_step_end(self, i, self.scheduler.sigmas[i], callback_kwargs)

Problem:
Both pipelines call callback_on_step_end(self, i, self.scheduler.sigmas[i], ...). The callback API and other pipelines pass the current timestep t.

Impact:
User callbacks that inspect timestep values, implement custom stopping, or coordinate with scheduler timesteps receive the wrong quantity.

Reproduction:

importtorchfromdiffusersimportAutoencoderKL, CogView4Pipeline, CogView4Transformer2DModel, FlowMatchEulerDiscreteSchedulertransformer=CogView4Transformer2DModel(
patch_size=2, in_channels=4, out_channels=4, num_layers=1,
attention_head_dim=4, num_attention_heads=2, text_embed_dim=8,
time_embed_dim=8, condition_dim=4, sample_size=8,
)
vae=AutoencoderKL(
block_out_channels=[32, 64], in_channels=3, out_channels=3,
down_block_types=["DownEncoderBlock2D", "DownEncoderBlock2D"],
up_block_types=["UpDecoderBlock2D", "UpDecoderBlock2D"],
latent_channels=4, sample_size=32,
)
scheduler=FlowMatchEulerDiscreteScheduler(use_dynamic_shifting=True)
pipe=CogView4Pipeline(None, None, vae, transformer, scheduler)
pipe.set_progress_bar_config(disable=True)
seen= []
defcallback(pipe, i, t, kwargs):
seen.append((float(t), float(pipe.scheduler.timesteps[i])))
returnkwargspipe(
prompt_embeds=torch.randn(1, 4, 8),
num_inference_steps=2,
guidance_scale=1.0,
height=16,
width=16,
output_type="latent",
callback_on_step_end=callback,
)
assertseen[0][0] ==seen[0][1], seen

Relevant precedent:

latents=latents.to(latents_dtype)
ifcallback_on_step_endisnotNone:
callback_kwargs= {}
forkincallback_on_step_end_tensor_inputs:
callback_kwargs[k] =locals()[k]
callback_outputs=callback_on_step_end(self, i, t, callback_kwargs)

latents=latents.to(latents_dtype)
ifcallback_on_step_endisnotNone:
callback_kwargs= {}
forkincallback_on_step_end_tensor_inputs:
callback_kwargs[k] =locals()[k]
callback_outputs=callback_on_step_end(self, i, t, callback_kwargs)

Suggested fix:

callback_outputs=callback_on_step_end(self, i, t, callback_kwargs)

Issue 5: Control pipeline is missing base CogView4 pipeline capabilities

Affected code:

classCogView4Pipeline(DiffusionPipeline, CogView4LoraLoaderMixin):

classCogView4ControlPipeline(DiffusionPipeline):

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

hidden_states=latent_model_input,
encoder_hidden_states=prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
# perform guidance
ifself.do_classifier_free_guidance:
noise_pred_uncond=self.transformer(
hidden_states=latent_model_input,
encoder_hidden_states=negative_prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,

from diffusers import CogView4ControlPipeline
from diffusers.utils import load_image
fromPILimport Image
import numpy as np
import torch
pipe = CogView4ControlPipeline.from_pretrained("THUDM/CogView4-6B", torch_dtype=torch.bfloat16).to("cuda")
pipe.load_lora_weights("...") # change this.
open_pose = OpenposeDetector.from_pretrained("lllyasviel/Annotators")
# prepare pose condition.
url ="https://huggingface.co/Adapter/t2iadapter/resolve/main/people.jpg"
image = load_image(url)
image = open_pose(image, detect_resolution=512, image_resolution=1024)
image = np.array(image)[:, :, ::-1]
image = Image.fromarray(np.uint8(image))
prompt ="A couple, 4k photo, highly detailed"
gen_images = pipe(
prompt=prompt,
control_image=image,
num_inference_steps=50,
joint_attention_kwargs={"scale": 0.9},

Problem:
CogView4ControlPipeline does not inherit CogView4LoraLoaderMixin, although the control README shows pipe.load_lora_weights(...). It also retains the old strict prompt/negative embed shape check, even though CogView4 runs cond/uncond in separate transformer calls and the base pipeline was already relaxed. Finally, the control denoising loop does not set "cond" / "uncond" cache contexts; that cache-context facet is already tracked generally by issue #12760.

Impact:
Control LoRA loading is unavailable, split text-encoding workflows fail for valid prompt/negative embeddings with different sequence lengths, and cache hooks can share state between conditional and unconditional passes.

Reproduction:

importinspectimporttorchfromdiffusersimportCogView4ControlPipelineprint("has load_lora_weights:", hasattr(CogView4ControlPipeline, "load_lora_weights"))
pipe=object.__new__(CogView4ControlPipeline)
pipe._callback_tensor_inputs= ["latents", "prompt_embeds", "negative_prompt_embeds"]
try:
pipe.check_inputs(
prompt=None,
height=16,
width=16,
negative_prompt=None,
callback_on_step_end_tensor_inputs=["latents"],
prompt_embeds=torch.randn(1, 12, 8),
negative_prompt_embeds=torch.randn(1, 4, 8),
)
exceptExceptionase:
print(type(e).__name__, e)
source=inspect.getsource(CogView4ControlPipeline.__call__)
print("uses cache_context:", 'cache_context("cond")'insourceand'cache_context("uncond")'insource)

Relevant precedent:

classCogView4Pipeline(DiffusionPipeline, CogView4LoraLoaderMixin):

withself.transformer.cache_context("cond"):
noise_pred_cond=self.transformer(
hidden_states=latent_model_input,
encoder_hidden_states=prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]
# perform guidance
ifself.do_classifier_free_guidance:
withself.transformer.cache_context("uncond"):
noise_pred_uncond=self.transformer(
hidden_states=latent_model_input,
encoder_hidden_states=negative_prompt_embeds,
timestep=timestep,
original_size=original_size,
target_size=target_size,
crop_coords=crops_coords_top_left,
attention_kwargs=attention_kwargs,
return_dict=False,
)[0]

Related prior base-pipeline fix: #10966
Duplicate for cache contexts: #12760

Suggested fix:

from ...loadersimportCogView4LoraLoaderMixinclassCogView4ControlPipeline(DiffusionPipeline, CogView4LoraLoaderMixin):
...
ifprompt_embedsisnotNoneandnegative_prompt_embedsisnotNone:
ifprompt_embeds.shape[0] !=negative_prompt_embeds.shape[0]:
raiseValueError(...)
ifprompt_embeds.shape[-1] !=negative_prompt_embeds.shape[-1]:
raiseValueError(...)
withself.transformer.cache_context("cond"):
noise_pred_cond=self.transformer(...)
withself.transformer.cache_context("uncond"):
noise_pred_uncond=self.transformer(...)

Issue 6: Lazy export advertises the wrong output class

Affected code:

_import_structure= {"pipeline_output": ["CogView4PlusPipelineOutput"]}

classCogView4PipelineOutput(BaseOutput):
"""
Output class for CogView3 pipelines.

Problem:
pipelines/cogview4/__init__.py exports CogView4PlusPipelineOutput, but pipeline_output.py defines CogView4PipelineOutput. The output docstring also says CogView3.

Impact:
from diffusers.pipelines.cogview4 import CogView4PipelineOutput fails, and autodoc/lazy import metadata is wrong.

Reproduction:

fromdiffusers.pipelines.cogview4importCogView4PipelineOutputprint(CogView4PipelineOutput)

Relevant precedent:

_import_structure= {"pipeline_output": ["CogView3PlusPipelineOutput"]}

classCogView3PipelineOutput(BaseOutput):
"""
Output class for CogView3 pipelines.

Suggested fix:

_import_structure= {"pipeline_output": ["CogView4PipelineOutput"]}

Also update the output docstring to say CogView4.

Issue 7: Control example documentation/script has runnable breakages

Affected code:

accelerate launch train_control_lora_cogview4.py \

joint_attention_kwargs={"scale": 0.9},

ifargs.only_target_transformer_blocks:
cogview4_transformer.patch_embed.proj.requires_grad_(True)
forname, moduleincogview4_transformer.named_modules():
if"transformer_blocks"inname:
module.requires_grad_(True)
else:
module.requirs_grad_(False)

Problem:
The README’s primary LoRA command references train_control_lora_cogview4.py, but that file is not present. The inference snippet passes joint_attention_kwargs, while the pipeline argument is attention_kwargs. The full fine-tuning script has module.requirs_grad_(False), which fails when --only_target_transformer_blocks is used.

Impact:
Users following the example cannot launch the documented LoRA training path, can pass an unsupported inference kwarg, and can hit an AttributeError in the full fine-tuning script.

Reproduction:

frompathlibimportPathimporttorch.nnasnnassertPath("examples/cogview4-control/train_control_lora_cogview4.py").exists()
nn.Linear(1, 1).requirs_grad_(False)

Relevant precedent:
The README itself says inference is performed with CogView4ControlPipeline, whose signature exposes attention_kwargs, not joint_attention_kwargs.

Suggested fix:

# train_control_cogview4.pymodule.requires_grad_(False)

Add the missing LoRA script or remove/update that section, and change the README inference kwarg to:

attention_kwargs={"scale": 0.9}

Issue 8: Slow tests and ControlPipeline tests are missing

Affected code:

classCogView4PipelineFastTests(PipelineTesterMixin, unittest.TestCase):

Problem:
The CogView4 test file only defines CogView4PipelineFastTests. There is no @slow CogView4 integration test, and there is no fast or slow coverage for CogView4ControlPipeline.

Impact:
The control pipeline can regress independently from the base pipeline, and real checkpoint loading/inference is untested. This also leaves the slow-test requirement for the target family unmet.

Reproduction:

frompathlibimportPathtest_files=list(Path("tests").rglob("*cogview4*.py"))
text="\n".join(path.read_text(encoding="utf-8") forpathintest_files)
print("test files:", [str(p) forpintest_files])
print("@slow present:", "@slow"intext)
print("CogView4ControlPipeline tested:", "CogView4ControlPipeline"intext)

Relevant precedent:

@slow
@require_torch_accelerator
classCogView3PlusPipelineIntegrationTests(unittest.TestCase):
prompt="A painting of a squirrel eating a burger."
defsetUp(self):
super().setUp()
gc.collect()
backend_empty_cache(torch_device)
deftearDown(self):
super().tearDown()
gc.collect()
backend_empty_cache(torch_device)
deftest_cogview3plus(self):
generator=torch.Generator("cpu").manual_seed(0)
pipe=CogView3PlusPipeline.from_pretrained("THUDM/CogView3Plus-3b", torch_dtype=torch.float16)
pipe.enable_model_cpu_offload(device=torch_device)
prompt=self.prompt
images=pipe(
prompt=prompt,

Suggested fix:
Add a CogView4ControlPipelineFastTests class with tiny synthetic components/control images, and add at least one @slow smoke test for the released CogView4 checkpoint family, including the control pipeline when the checkpoint is available to CI.

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