glm_image model/pipeline review
Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423
Review performed against the repository review rules.
Duplicate-search status: searched huggingface/diffusers Issues and PRs for glm_image, GLM-Image, GlmImagePipeline, GlmImageTransformer2DModel, attention_mask, prompt_embeds dtype device, check_inputs width, transformer version gating, and slow-test coverage. I found no duplicates for the findings below. Related but not duplicates: PR #12974 adjusted GLM transformer-version gating, PR #13007 added batch support, PR #13344 added model tests, and issue #13227 tracks an MPS loading corruption issue.
Issue 1: Attention masks do not actually mask padded text 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) |
| attention_mask=torch.tensor( |
| [[1] *len(input_ids_) + [0] * (max_length-len(input_ids_)) forinput_ids_ininput_ids], |
| device=device, |
| ) |
| input_ids=torch.tensor( |
| [ |
| input_ids_+ [self.tokenizer.pad_token_id] * (max_length-len(input_ids_)) |
| forinput_ids_ininput_ids |
| ], |
| device=device, |
| ) |
| outputs=self.text_encoder(input_ids, attention_mask=attention_mask) |
| glyph_embeds=outputs.last_hidden_state[attention_mask.bool()].unsqueeze(0) |
| all_glyph_embeds.append(glyph_embeds) |
| |
| # Pad to same sequence length and stack (use left padding to match transformers) |
| max_seq_len=max(emb.size(1) forembinall_glyph_embeds) |
| padded_embeds= [] |
| forembinall_glyph_embeds: |
| ifemb.size(1) <max_seq_len: |
| pad=torch.zeros(emb.size(0), max_seq_len-emb.size(1), emb.size(2), device=device, dtype=emb.dtype) |
| emb=torch.cat([pad, emb], dim=1) # left padding |
| padded_embeds.append(emb) |
| |
| glyph_embeds=torch.cat(padded_embeds, dim=0) |
| returnglyph_embeds.to(device=device, dtype=dtype) |
| noise_pred_cond=self.transformer( |
| hidden_states=latent_model_input, |
| encoder_hidden_states=prompt_embeds, |
| prior_token_id=prior_token_ids, |
| prior_token_drop=prior_token_drop_cond, |
| timestep=timestep, |
| target_size=target_size, |
| crop_coords=crops_coords_top_left, |
| attention_kwargs=attention_kwargs, |
| return_dict=False, |
| kv_caches=kv_caches, |
| )[0].float() |
| |
| # perform guidance |
| ifself.do_classifier_free_guidance: |
| ifprior_token_image_ids_per_sampleisnotNone: |
| kv_caches.set_mode("skip") |
| noise_pred_uncond=self.transformer( |
| hidden_states=latent_model_input, |
| encoder_hidden_states=negative_prompt_embeds, |
| prior_token_id=prior_token_ids, |
| prior_token_drop=prior_token_drop_uncond, |
| timestep=timestep, |
| target_size=target_size, |
| crop_coords=crops_coords_top_left, |
| attention_kwargs=attention_kwargs, |
| return_dict=False, |
| kv_caches=kv_caches, |
Problem:
GlmImageAttnProcessor converts a boolean padding mask into a dense float 0/1 tensor. SDPA treats float masks as additive attention bias, so 0 does not block a token. The pipeline also discards the glyph encoder padding mask after constructing padded glyph embeddings, so batched variable-length glyph prompts have no valid text mask passed to the transformer.
Impact:
Masked or padded text tokens can still affect image tokens. This can make batched outputs differ from equivalent single-prompt outputs and makes the public attention_mask argument misleading. It also prevents the bool-mask varlen path described in the review rules.
Reproduction:
importtorchfromdiffusers.models.attention_processorimportAttentionfromdiffusers.models.transformers.transformer_glm_imageimportGlmImageAttnProcessorattn=Attention(query_dim=4, heads=1, dim_head=4, out_dim=4, bias=False, processor=GlmImageAttnProcessor())
withtorch.no_grad():
attn.to_q.weight.zero_()
attn.to_k.weight.zero_()
attn.to_v.weight.copy_(torch.eye(4))
attn.to_out[0].weight.copy_(torch.eye(4))
encoder_hidden_states=torch.tensor([[[0., 0., 0., 0.], [1000., 0., 0., 0.]]])
hidden_states=torch.zeros(1, 1, 4)
attention_mask=torch.tensor([[True, False]])
image_out, _=attn(
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
attention_mask=attention_mask,
)
print(image_out[0, 0, 0].item()) # current: ~155.66; expected: 0 if token 2 is masked
Relevant precedent:
| 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 |
| 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] |
Suggested fix:
# In GlmImageAttnProcessor.__call__, keep a bool key mask instead of a float QK matrix.ifattention_maskisnotNone:
attention_mask=attention_mask.to(device=query.device, dtype=torch.bool)
cached_seq_length=key.shape[1] -text_seq_length-image_seq_lengthcache_mask=torch.ones((batch_size, cached_seq_length), device=query.device, dtype=torch.bool)
image_mask=torch.ones((batch_size, image_seq_length), device=query.device, dtype=torch.bool)
attention_mask=torch.cat([cache_mask, attention_mask, image_mask], dim=1)
Also return a glyph padding mask from _get_glyph_embeds and pass it through the conditional and unconditional transformer calls.
Issue 2: Width validation accepts invalid resolutions and silently truncates latents
Affected code:
| if ( |
| heightisnotNone |
| andheight% (self.vae_scale_factor*self.transformer.config.patch_size*2) !=0 |
| orwidthisnotNone |
| andwidth% (self.transformer.config.patch_size*2) !=0 |
| ): |
| # GLM-Image uses 32× downsampling, so the image dimensions must be multiples of 32. |
| raiseValueError( |
| f"`height` and `width` have to be divisible by {self.vae_scale_factor*4} but are {height} and {width}." |
| ) |
Problem:
check_inputs validates height with vae_scale_factor * patch_size * 2, but validates width only with patch_size * 2. With default GLM settings this accepts widths divisible by 4, even though the pipeline later floors latent width by width // vae_scale_factor.
Impact:
Invalid widths pass validation and produce latents for a smaller decoded width than requested.
Reproduction:
importtorchfromdiffusersimportGlmImagePipelineclassConfig:
patch_size=2classTransformer:
config=Config()
pipe=object.__new__(GlmImagePipeline)
pipe.vae_scale_factor=8pipe.transformer=Transformer()
pipe._callback_tensor_inputs= ["latents", "prompt_embeds"]
pipe.check_inputs(prompt="x", height=32, width=20, callback_on_step_end_tensor_inputs=["latents"])
latents=pipe.prepare_latents(1, 4, 32, 20, torch.float32, torch.device("cpu"), torch.Generator().manual_seed(0))
print(latents.shape[-1] *pipe.vae_scale_factor) # 16, not requested width 20Relevant precedent:
| ifheight%16!=0orwidth%16!=0: |
| raiseValueError(f"`height` and `width` have to be divisible by 16 but are {height} and {width}.") |
Suggested fix:
multiple_of=self.vae_scale_factor*self.transformer.config.patch_size*2if (heightisnotNoneandheight%multiple_of!=0) or (widthisnotNoneandwidth%multiple_of!=0):
raiseValueError(f"`height` and `width` have to be divisible by {multiple_of} but are {height} and {width}.")Issue 3: Precomputed conditioning tensors are not moved or cast
Affected code:
| ifprompt_embedsisNone: |
| prompt_embeds=self._get_glyph_embeds(prompt, max_sequence_length, device, dtype) |
| |
| # Repeat embeddings for num_images_per_prompt |
| ifnum_images_per_prompt>1: |
| prompt_embeds=prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0) |
| |
| # For GLM-Image, negative_prompt must be "" instead of None |
| ifdo_classifier_free_guidanceandnegative_prompt_embedsisNone: |
| negative_prompt="" |
| negative_prompt=batch_size* [negative_prompt] ifisinstance(negative_prompt, str) elsenegative_prompt |
| negative_prompt_embeds=self._get_glyph_embeds(negative_prompt, max_sequence_length, device, dtype) |
| |
| ifnum_images_per_prompt>1: |
| negative_prompt_embeds=negative_prompt_embeds.repeat_interleave(num_images_per_prompt, dim=0) |
| |
| returnprompt_embeds, negative_prompt_embeds |
| else: |
| # User provided prior_token_ids directly (from generate_prior_tokens) |
| prior_token_image_ids_per_sample=prior_token_image_ids |
| source_image_grid_thw_per_sample=source_image_grid_thw |
Problem:
When users pass prompt_embeds, negative_prompt_embeds, or prior token tensors directly, the pipeline returns/uses them as-is instead of normalizing them to the execution device and dtype.
Impact:
Precomputed embeddings can fail at the transformer with dtype or device mismatches. Prior token tensors generated on CPU can also fail when the pipeline is on an accelerator.
Reproduction:
importtorchfromdiffusersimportGlmImagePipelinepipe=object.__new__(GlmImagePipeline)
prompt_embeds=torch.randn(1, 2, 4, dtype=torch.float64)
out, _=pipe.encode_prompt(
prompt=None,
do_classifier_free_guidance=False,
prompt_embeds=prompt_embeds,
device=torch.device("cpu"),
dtype=torch.float32,
)
print(out.dtype, outisprompt_embeds) # torch.float64 TrueRelevant precedent:
| prompt_embeds=self.text_encoder(text_input_ids.to(device), output_hidden_states=True).hidden_states[-2] |
| |
| prompt_embeds=prompt_embeds.to(dtype=dtype, device=device) |
Suggested fix:
ifprompt_embedsisNone:
prompt_embeds=self._get_glyph_embeds(prompt, max_sequence_length, device, dtype)
else:
prompt_embeds=prompt_embeds.to(device=device, dtype=dtype)
ifnegative_prompt_embedsisnotNone:
negative_prompt_embeds=negative_prompt_embeds.to(device=device, dtype=dtype)
ifprior_token_idsisnotNone:
prior_token_ids=prior_token_ids.to(device=device)
ifprior_token_image_idsisnotNone:
prior_token_image_ids= [x.to(device=device) forxinprior_token_image_ids]
ifsource_image_grid_thwisnotNone:
source_image_grid_thw= [x.to(device=device) forxinsource_image_grid_thw]
Issue 4: Transformers version gates are inconsistent with required GLM classes
Affected code:
| # Because it's not released in stable as of 13/01/2026. So this is just a proxy. |
| GlmImageProcessor=ProcessorMixin |
| GlmImageForConditionalGeneration=PreTrainedModel |
| ifis_transformers_version(">=", "5.0.0.dev0"): |
| fromtransformersimportGlmImageForConditionalGeneration, GlmImageProcessor |
| ifis_transformers_available() andis_transformers_version(">=", "4.57.4"): |
| try: |
| fromtransformersimportGlmImageForConditionalGeneration, GlmImageProcessor |
| |
| _additional_imports["GlmImageForConditionalGeneration"] =GlmImageForConditionalGeneration |
| _additional_imports["GlmImageProcessor"] =GlmImageProcessor |
| exceptImportError: |
| pass |
| ifis_transformers_version(">=", "5.0.0.dev0"): |
| fromtransformersimportGlmImageConfig, GlmImageForConditionalGeneration, GlmImageProcessor |
| |
| |
| enable_full_determinism() |
| |
| |
| @require_transformers_version_greater("4.57.4") |
| @require_torch_accelerator |
Problem:
The pipeline file only imports real GlmImageProcessor / GlmImageForConditionalGeneration for transformers >= 5.0.0.dev0, but the package init tries >= 4.57.4 and the fast tests require only > 4.57.4.
Impact:
With transformers==4.57.6, the pipeline is importable but uses ProcessorMixin / PreTrainedModel placeholders, and the test decorator would allow tests whose GLM classes are not imported.
Reproduction:
importtransformersfromdiffusers.utilsimportis_transformers_versionfromdiffusers.pipelines.glm_image.pipeline_glm_imageimportGlmImageProcessor, GlmImageForConditionalGenerationfromtransformersimportProcessorMixin, PreTrainedModelprint(transformers.__version__)
print(is_transformers_version(">", "4.57.4")) # True in this envprint(is_transformers_version(">=", "5.0.0.dev0")) # Falseprint(hasattr(transformers, "GlmImageProcessor")) # Falseprint(GlmImageProcessorisProcessorMixin, GlmImageForConditionalGenerationisPreTrainedModel)Relevant precedent:
Related prior version-gating PR: #12974
Suggested fix:
GLM_IMAGE_TRANSFORMERS_MIN_VERSION="5.0.0.dev0"# or the first released transformers version with these classesifis_transformers_available() andis_transformers_version(">=", GLM_IMAGE_TRANSFORMERS_MIN_VERSION):
fromtransformersimportGlmImageForConditionalGeneration, GlmImageProcessorelse:
raiseOptionalDependencyNotAvailable()Use the same predicate in pipeline lazy imports and tests.
Issue 5: Slow tests are missing, and current fast coverage is not portable
Affected code:
| @require_transformers_version_greater("4.57.4") |
| @require_torch_accelerator |
| classGlmImagePipelineFastTests(PipelineTesterMixin, unittest.TestCase): |
| processor=GlmImageProcessor.from_pretrained("zai-org/GLM-Image", subfolder="processor") |
| A Diffusion Transformer model for 2D data from [GlmImageTransformer2DModel] (TODO). |
| classGlmImagePipelineOutput(BaseOutput): |
| """ |
| Output class for CogView3 pipelines. |
Problem:
There are no GLM Image slow tests. The pipeline “fast” tests are decorated with @require_torch_accelerator even though they run on CPU, and they load the processor from zai-org/GLM-Image instead of an internal tiny fixture. The model docs still contain a TODO, and the pipeline output docstring says CogView3.
Impact:
CPU CI can skip the pipeline fast suite, slow end-to-end loading of zai-org/GLM-Image is untested, and docs have stale placeholders.
Reproduction:
frompathlibimportPathtest_text=Path("tests/pipelines/glm_image/test_glm_image.py").read_text(encoding="utf-8")
model_doc=Path("docs/source/en/api/models/glm_image_transformer2d.md").read_text(encoding="utf-8")
print("@slow present:", "@slow"intest_text)
print("fast class requires accelerator:", "@require_torch_accelerator"intest_text)
print("fast test downloads full GLM processor:", "zai-org/GLM-Image"intest_text)
print("model docs still contain TODO:", "TODO"inmodel_doc)Relevant precedent:
| tokenizer=Qwen2Tokenizer.from_pretrained("hf-internal-testing/tiny-random-Qwen2VLForConditionalGeneration") |
Suggested fix:
Use an hf-internal-testing tiny GLM processor fixture for fast tests, remove the accelerator requirement from CPU-only fast tests, add at least one @slow smoke test for GlmImagePipeline.from_pretrained("zai-org/GLM-Image", ...), and replace the stale TODO/CogView3 doc text.
Test status: a tiny CPU GlmImageTransformer2DModel forward pass succeeded. Targeted pytest collection for GLM model/pipeline tests failed in this local .venv because the installed Torch build lacks torch._C._distributed_c10d, so I could not use pytest results as signal for this audit.
glm_imagemodel/pipeline reviewCommit tested:
0f1abc4ae8b0eb2a3b40e82a310507281144c423Review performed against the repository review rules.
Duplicate-search status: searched
huggingface/diffusersIssues and PRs forglm_image,GLM-Image,GlmImagePipeline,GlmImageTransformer2DModel,attention_mask,prompt_embeds dtype device,check_inputs width, transformer version gating, and slow-test coverage. I found no duplicates for the findings below. Related but not duplicates: PR #12974 adjusted GLM transformer-version gating, PR #13007 added batch support, PR #13344 added model tests, and issue #13227 tracks an MPS loading corruption issue.Issue 1: Attention masks do not actually mask padded text tokens
Affected code:
diffusers/src/diffusers/models/transformers/transformer_glm_image.py
Lines 317 to 325 in 0f1abc4
diffusers/src/diffusers/pipelines/glm_image/pipeline_glm_image.py
Lines 518 to 543 in 0f1abc4
diffusers/src/diffusers/pipelines/glm_image/pipeline_glm_image.py
Lines 972 to 999 in 0f1abc4
Problem:
GlmImageAttnProcessorconverts a boolean padding mask into a dense float0/1tensor. SDPA treats float masks as additive attention bias, so0does not block a token. The pipeline also discards the glyph encoder padding mask after constructing padded glyph embeddings, so batched variable-length glyph prompts have no valid text mask passed to the transformer.Impact:
Masked or padded text tokens can still affect image tokens. This can make batched outputs differ from equivalent single-prompt outputs and makes the public
attention_maskargument misleading. It also prevents the bool-mask varlen path described in the review rules.Reproduction:
Relevant precedent:
diffusers/src/diffusers/models/transformers/transformer_qwenimage.py
Lines 946 to 952 in 0f1abc4
diffusers/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py
Lines 695 to 704 in 0f1abc4
Suggested fix:
Also return a glyph padding mask from
_get_glyph_embedsand pass it through the conditional and unconditional transformer calls.Issue 2: Width validation accepts invalid resolutions and silently truncates latents
Affected code:
diffusers/src/diffusers/pipelines/glm_image/pipeline_glm_image.py
Lines 633 to 642 in 0f1abc4
Problem:
check_inputsvalidates height withvae_scale_factor * patch_size * 2, but validates width only withpatch_size * 2. With default GLM settings this accepts widths divisible by4, even though the pipeline later floors latent width bywidth // vae_scale_factor.Impact:
Invalid widths pass validation and produce latents for a smaller decoded width than requested.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/cogview4/pipeline_cogview4.py
Lines 328 to 329 in 0f1abc4
Suggested fix:
Issue 3: Precomputed conditioning tensors are not moved or cast
Affected code:
diffusers/src/diffusers/pipelines/glm_image/pipeline_glm_image.py
Lines 584 to 600 in 0f1abc4
diffusers/src/diffusers/pipelines/glm_image/pipeline_glm_image.py
Lines 833 to 836 in 0f1abc4
Problem:
When users pass
prompt_embeds,negative_prompt_embeds, or prior token tensors directly, the pipeline returns/uses them as-is instead of normalizing them to the execution device and dtype.Impact:
Precomputed embeddings can fail at the transformer with dtype or device mismatches. Prior token tensors generated on CPU can also fail when the pipeline is on an accelerator.
Reproduction:
Relevant precedent:
diffusers/src/diffusers/pipelines/cogview4/pipeline_cogview4.py
Lines 216 to 218 in 0f1abc4
Suggested fix:
Issue 4: Transformers version gates are inconsistent with required GLM classes
Affected code:
diffusers/src/diffusers/pipelines/glm_image/pipeline_glm_image.py
Lines 36 to 40 in 0f1abc4
diffusers/src/diffusers/pipelines/glm_image/__init__.py
Lines 20 to 27 in 0f1abc4
diffusers/tests/pipelines/glm_image/test_glm_image.py
Lines 29 to 37 in 0f1abc4
Problem:
The pipeline file only imports real
GlmImageProcessor/GlmImageForConditionalGenerationfortransformers >= 5.0.0.dev0, but the package init tries>= 4.57.4and the fast tests require only> 4.57.4.Impact:
With
transformers==4.57.6, the pipeline is importable but usesProcessorMixin/PreTrainedModelplaceholders, and the test decorator would allow tests whose GLM classes are not imported.Reproduction:
Relevant precedent:
Related prior version-gating PR: #12974
Suggested fix:
Use the same predicate in pipeline lazy imports and tests.
Issue 5: Slow tests are missing, and current fast coverage is not portable
Affected code:
diffusers/tests/pipelines/glm_image/test_glm_image.py
Lines 36 to 38 in 0f1abc4
diffusers/tests/pipelines/glm_image/test_glm_image.py
Line 90 in 0f1abc4
diffusers/docs/source/en/api/models/glm_image_transformer2d.md
Line 14 in 0f1abc4
diffusers/src/diffusers/pipelines/glm_image/pipeline_output.py
Lines 10 to 12 in 0f1abc4
Problem:
There are no GLM Image slow tests. The pipeline “fast” tests are decorated with
@require_torch_acceleratoreven though they run on CPU, and they load the processor fromzai-org/GLM-Imageinstead of an internal tiny fixture. The model docs still contain a TODO, and the pipeline output docstring says CogView3.Impact:
CPU CI can skip the pipeline fast suite, slow end-to-end loading of
zai-org/GLM-Imageis untested, and docs have stale placeholders.Reproduction:
Relevant precedent:
diffusers/tests/pipelines/qwenimage/test_qwenimage.py
Line 117 in 0f1abc4
Suggested fix:
Use an
hf-internal-testingtiny GLM processor fixture for fast tests, remove the accelerator requirement from CPU-only fast tests, add at least one@slowsmoke test forGlmImagePipeline.from_pretrained("zai-org/GLM-Image", ...), and replace the stale TODO/CogView3 doc text.Test status: a tiny CPU
GlmImageTransformer2DModelforward pass succeeded. Targeted pytest collection for GLM model/pipeline tests failed in this local.venvbecause the installed Torch build lackstorch._C._distributed_c10d, so I could not use pytest results as signal for this audit.