Skip to content

kolors model/pipeline review #13612

Description

@hlky

kolors model/pipeline review

Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423

Review performed against the repository review rules.

Target files reviewed: kolors/__init__.py, pipeline_kolors.py, pipeline_kolors_img2img.py, pipeline_output.py, text_encoder.py, tokenizer.py, plus Kolors public exports, docs/examples references, and tests.

Duplicate search status: searched GitHub Issues and PRs for kolors, affected class/function/file names, and the specific failure modes below. Existing related items found: sentencepiece import issue #9034, Kolors from-single-file issue #10207 / PR #10215, and Kolors LoRA PR #11198. No duplicate found for the specific prompt-embedding, max_sequence_length, img2img offload, output export, or original_rope findings below.

Test note: targeted reproductions ran under .venv. Full fast-test collection with python -m pytest tests/pipelines/kolors/test_kolors.py tests/pipelines/kolors/test_kolors_img2img.py -q failed in this local environment because the installed torch build lacks torch._C._distributed_c10d.

Issue 1: KolorsPipelineOutput is not exported from diffusers.pipelines.kolors

Affected code:

_import_structure= {}
try:
ifnot (is_transformers_available() andis_torch_available()) andis_sentencepiece_available():
raiseOptionalDependencyNotAvailable()
exceptOptionalDependencyNotAvailable:
from ...utilsimportdummy_torch_and_transformers_and_sentencepiece_objects# noqa F403
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_and_sentencepiece_objects))
else:
_import_structure["pipeline_kolors"] = ["KolorsPipeline"]
_import_structure["pipeline_kolors_img2img"] = ["KolorsImg2ImgPipeline"]
_import_structure["text_encoder"] = ["ChatGLMModel"]
_import_structure["tokenizer"] = ["ChatGLMTokenizer"]

classKolorsPipelineOutput(BaseOutput):
"""
Output class for Kolors pipelines.
Args:
images (`list[PIL.Image.Image]` or `np.ndarray`)
list of denoised PIL images of length `batch_size` or numpy array of shape `(batch_size, height, width,
num_channels)`. PIL images or numpy array present the denoised images of the diffusion pipeline.
"""
images: list[PIL.Image.Image] |np.ndarray

Problem:
KolorsPipelineOutput is defined and referenced in docstrings as ~pipelines.kolors.KolorsPipelineOutput, but kolors/__init__.py never adds pipeline_output to _import_structure.

Impact:
Public subpackage import fails and autodoc cross-references can resolve inconsistently.

Reproduction:

fromdiffusers.pipelines.kolorsimportKolorsPipelineOutput# ImportError: cannot import name 'KolorsPipelineOutput'

Relevant precedent:
stable_diffusion, stable_diffusion_xl, qwenimage, and flux export their pipeline output classes from the subpackage __init__.py.

Suggested fix:

_import_structure["pipeline_output"] = ["KolorsPipelineOutput"]
# TYPE_CHECKING branchfrom .pipeline_outputimportKolorsPipelineOutput

Issue 2: Kolors subpackage lazy import still breaks when sentencepiece is missing

Affected code:

ifnot (is_transformers_available() andis_torch_available()) andis_sentencepiece_available():
raiseOptionalDependencyNotAvailable()
exceptOptionalDependencyNotAvailable:
from ...utilsimportdummy_torch_and_transformers_and_sentencepiece_objects# noqa F403
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_and_sentencepiece_objects))
else:
_import_structure["pipeline_kolors"] = ["KolorsPipeline"]
_import_structure["pipeline_kolors_img2img"] = ["KolorsImg2ImgPipeline"]
_import_structure["text_encoder"] = ["ChatGLMModel"]
_import_structure["tokenizer"] = ["ChatGLMTokenizer"]

ifnot (is_transformers_available() andis_torch_available()) andis_sentencepiece_available():
raiseOptionalDependencyNotAvailable()
exceptOptionalDependencyNotAvailable:
from ...utils.dummy_torch_and_transformers_and_sentencepiece_objectsimport*
else:
from .pipeline_kolorsimportKolorsPipeline
from .pipeline_kolors_img2imgimportKolorsImg2ImgPipeline

Problem:
The dependency guard uses if not (is_transformers_available() and is_torch_available()) and is_sentencepiece_available(). That only raises when torch/transformers are missing and sentencepiece is present. If sentencepiece is missing, Kolors exposes real lazy modules that import sentencepiece.

Impact:
Direct imports from diffusers.pipelines.kolors can fail with a raw lazy-module import error instead of the normal backend message. This is the same failure class as existing issue #9034, but the subpackage guard is still malformed here.

Reproduction:

importbuiltins, importlib, sysimportdiffusers.utilsasutilsutils.is_torch_available=lambda: Trueutils.is_transformers_available=lambda: Trueutils.is_sentencepiece_available=lambda: Falsefornameinlist(sys.modules):
ifname=="diffusers.pipelines.kolors"orname.startswith("diffusers.pipelines.kolors."):
delsys.modules[name]
kolors=importlib.import_module("diffusers.pipelines.kolors")
real_import=builtins.__import__defblocked_import(name, *args, **kwargs):
ifname=="sentencepiece":
raiseModuleNotFoundError("No module named 'sentencepiece'")
returnreal_import(name, *args, **kwargs)
builtins.__import__=blocked_importtry:
kolors.ChatGLMTokenizerfinally:
builtins.__import__=real_import

Relevant precedent:
The parent package has the correct condition:

ifnot (is_torch_available() andis_transformers_available() andis_sentencepiece_available()):
raiseOptionalDependencyNotAvailable()
exceptOptionalDependencyNotAvailable:
from ..utilsimport (
dummy_torch_and_transformers_and_sentencepiece_objects,
)
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_and_sentencepiece_objects))
else:
_import_structure["kolors"] = [
"KolorsPipeline",
"KolorsImg2ImgPipeline",

Suggested fix:

ifnot (is_transformers_available() andis_torch_available() andis_sentencepiece_available()):
raiseOptionalDependencyNotAvailable()

Issue 3: max_sequence_length is validated but ignored by both pipeline calls

Affected code:

) =self.encode_prompt(
prompt=prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
do_classifier_free_guidance=self.do_classifier_free_guidance,
negative_prompt=negative_prompt,
prompt_embeds=prompt_embeds,
pooled_prompt_embeds=pooled_prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
)

) =self.encode_prompt(
prompt=prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
do_classifier_free_guidance=self.do_classifier_free_guidance,
negative_prompt=negative_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
)

Problem:
__call__ accepts and validates max_sequence_length, but does not pass it into encode_prompt, so tokenization always uses the default 256.

Impact:
Users cannot shorten prompt encoding for memory/performance or test non-default sequence lengths through the public pipeline API.

Reproduction:

importtorchfromdiffusersimportKolorsPipelineclassDummy(KolorsPipeline):
@propertydef_execution_device(self):
returntorch.device("cpu")
@propertydefdo_classifier_free_guidance(self):
returnFalsedefcheck_inputs(self, *args, **kwargs):
passdefencode_prompt(self, **kwargs):
print(kwargs.get("max_sequence_length"))
raiseRuntimeError("stop")
pipe=Dummy.__new__(Dummy)
pipe.default_sample_size=8pipe.vae_scale_factor=8try:
pipe(prompt="x", max_sequence_length=16)
exceptRuntimeError:
pass# Prints None, not 16.

Relevant precedent:
QwenImage forwards the call-time value:

max_sequence_length=max_sequence_length,
)
self._guidance_scale=guidance_scale
self._attention_kwargs=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
has_neg_prompt=negative_promptisnotNoneornegative_prompt_embedsisnotNone
iftrue_cfg_scale>1andnothas_neg_prompt:
logger.warning(
f"true_cfg_scale is passed as {true_cfg_scale}, but classifier-free guidance is not enabled since no negative_prompt is provided."
)
eliftrue_cfg_scale<=1andhas_neg_prompt:
logger.warning(
" negative_prompt is passed but classifier-free guidance is not enabled since true_cfg_scale <= 1"
)
do_true_cfg=true_cfg_scale>1andhas_neg_prompt
prompt_embeds, prompt_embeds_mask=self.encode_prompt(
prompt=prompt,
prompt_embeds=prompt_embeds,
prompt_embeds_mask=prompt_embeds_mask,
device=device,
num_images_per_prompt=num_images_per_prompt,
max_sequence_length=max_sequence_length,
)
ifdo_true_cfg:
negative_prompt_embeds, negative_prompt_embeds_mask=self.encode_prompt(
prompt=negative_prompt,
prompt_embeds=negative_prompt_embeds,
prompt_embeds_mask=negative_prompt_embeds_mask,
device=device,
num_images_per_prompt=num_images_per_prompt,
max_sequence_length=max_sequence_length,

Suggested fix:

) =self.encode_prompt(
...
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
max_sequence_length=max_sequence_length,
)

Issue 4: encode_prompt mishandles zeroed negatives and precomputed prompt embeds

Affected code:

zero_out_negative_prompt=negative_promptisNoneandself.config.force_zeros_for_empty_prompt
ifdo_classifier_free_guidanceandnegative_prompt_embedsisNoneandzero_out_negative_prompt:
negative_prompt_embeds=torch.zeros_like(prompt_embeds)

bs_embed=pooled_prompt_embeds.shape[0]
pooled_prompt_embeds=pooled_prompt_embeds.repeat(1, num_images_per_prompt).view(
bs_embed*num_images_per_prompt, -1
)
ifdo_classifier_free_guidance:
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds.repeat(1, num_images_per_prompt).view(
bs_embed*num_images_per_prompt, -1
)

zero_out_negative_prompt=negative_promptisNoneandself.config.force_zeros_for_empty_prompt
ifdo_classifier_free_guidanceandnegative_prompt_embedsisNoneandzero_out_negative_prompt:
negative_prompt_embeds=torch.zeros_like(prompt_embeds)

bs_embed=pooled_prompt_embeds.shape[0]
pooled_prompt_embeds=pooled_prompt_embeds.repeat(1, num_images_per_prompt).view(
bs_embed*num_images_per_prompt, -1
)
ifdo_classifier_free_guidance:
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds.repeat(1, num_images_per_prompt).view(
bs_embed*num_images_per_prompt, -1
)

Problem:
When force_zeros_for_empty_prompt=True, the code zeros negative_prompt_embeds but leaves negative_pooled_prompt_embeds=None, then calls .repeat(...). Separately, if users pass precomputed prompt_embeds and negative_prompt_embeds, only pooled embeds are repeated for num_images_per_prompt; sequence embeds stay at batch size 1.

Impact:
The zero-negative config path crashes. Precomputed embeddings with num_images_per_prompt > 1 later fail in UNet attention because latent batch and text batch do not match.

Reproduction:

importtorchfromtypesimportSimpleNamespacefromdiffusers.pipelines.kolors.pipeline_kolorsimportKolorsPipelinepipe=KolorsPipeline.__new__(KolorsPipeline)
pipe._internal_dict=SimpleNamespace(force_zeros_for_empty_prompt=True)
pipe.tokenizer=Nonepipe.text_encoder=Nonetry:
pipe.encode_prompt(
prompt=None,
device=torch.device("cpu"),
prompt_embeds=torch.randn(1, 4, 8),
pooled_prompt_embeds=torch.randn(1, 8),
do_classifier_free_guidance=True,
)
exceptExceptionase:
print(type(e).__name__, e)
pipe._internal_dict=SimpleNamespace(force_zeros_for_empty_prompt=False)
out=pipe.encode_prompt(
prompt=None,
device=torch.device("cpu"),
prompt_embeds=torch.randn(1, 4, 8),
pooled_prompt_embeds=torch.randn(1, 8),
negative_prompt_embeds=torch.randn(1, 4, 8),
negative_pooled_prompt_embeds=torch.randn(1, 8),
do_classifier_free_guidance=True,
num_images_per_prompt=2,
)
print([tuple(t.shape) fortinout])
# prompt/negative sequence embeds remain (1, 4, 8), pooled embeds become (2, 8).

Relevant precedent:
SDXL zeros the pooled negative embed and always duplicates prompt embeds after encoding/reuse:

# get unconditional embeddings for classifier free guidance
zero_out_negative_prompt=negative_promptisNoneandself.config.force_zeros_for_empty_prompt
ifdo_classifier_free_guidanceandnegative_prompt_embedsisNoneandzero_out_negative_prompt:
negative_prompt_embeds=torch.zeros_like(prompt_embeds)
negative_pooled_prompt_embeds=torch.zeros_like(pooled_prompt_embeds)
elifdo_classifier_free_guidanceandnegative_prompt_embedsisNone:
negative_prompt=negative_promptor""
negative_prompt_2=negative_prompt_2ornegative_prompt
# normalize str to list
negative_prompt=batch_size* [negative_prompt] ifisinstance(negative_prompt, str) elsenegative_prompt
negative_prompt_2= (
batch_size* [negative_prompt_2] ifisinstance(negative_prompt_2, str) elsenegative_prompt_2
)
uncond_tokens: list[str]
ifpromptisnotNoneandtype(prompt) isnottype(negative_prompt):
raiseTypeError(
f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
f" {type(prompt)}."
)
elifbatch_size!=len(negative_prompt):
raiseValueError(
f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
" the batch size of `prompt`."
)
else:
uncond_tokens= [negative_prompt, negative_prompt_2]
negative_prompt_embeds_list= []
fornegative_prompt, tokenizer, text_encoderinzip(uncond_tokens, tokenizers, text_encoders):
ifisinstance(self, TextualInversionLoaderMixin):
negative_prompt=self.maybe_convert_prompt(negative_prompt, tokenizer)
max_length=prompt_embeds.shape[1]
uncond_input=tokenizer(
negative_prompt,
padding="max_length",
max_length=max_length,
truncation=True,
return_tensors="pt",
)
negative_prompt_embeds=text_encoder(
uncond_input.input_ids.to(device),
output_hidden_states=True,
)
# We are only ALWAYS interested in the pooled output of the final text encoder
ifnegative_pooled_prompt_embedsisNoneandnegative_prompt_embeds[0].ndim==2:
negative_pooled_prompt_embeds=negative_prompt_embeds[0]
negative_prompt_embeds=negative_prompt_embeds.hidden_states[-2]
negative_prompt_embeds_list.append(negative_prompt_embeds)
negative_prompt_embeds=torch.concat(negative_prompt_embeds_list, dim=-1)
ifself.text_encoder_2isnotNone:
prompt_embeds=prompt_embeds.to(dtype=self.text_encoder_2.dtype, device=device)
else:
prompt_embeds=prompt_embeds.to(dtype=self.unet.dtype, device=device)
bs_embed, seq_len, _=prompt_embeds.shape
# duplicate text embeddings for each generation per prompt, using mps friendly method
prompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds=prompt_embeds.view(bs_embed*num_images_per_prompt, seq_len, -1)
ifdo_classifier_free_guidance:
# duplicate unconditional embeddings for each generation per prompt, using mps friendly method
seq_len=negative_prompt_embeds.shape[1]
ifself.text_encoder_2isnotNone:
negative_prompt_embeds=negative_prompt_embeds.to(dtype=self.text_encoder_2.dtype, device=device)
else:
negative_prompt_embeds=negative_prompt_embeds.to(dtype=self.unet.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_embedsisNoneandzero_out_negative_prompt:
negative_prompt_embeds=torch.zeros_like(prompt_embeds)
negative_pooled_prompt_embeds=torch.zeros_like(pooled_prompt_embeds)
bs_embed, seq_len, _=prompt_embeds.shapeprompt_embeds=prompt_embeds.repeat(1, num_images_per_prompt, 1).view(
bs_embed*num_images_per_prompt, seq_len, -1
)
ifdo_classifier_free_guidance:
seq_len=negative_prompt_embeds.shape[1]
negative_prompt_embeds=negative_prompt_embeds.repeat(1, num_images_per_prompt, 1).view(
bs_embed*num_images_per_prompt, seq_len, -1
)

Apply in pipeline_kolors.py, then propagate copied blocks.

Issue 5: KolorsImg2ImgPipeline.__call__ drops pooled prompt embeds

Affected code:

) =self.encode_prompt(
prompt=prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
do_classifier_free_guidance=self.do_classifier_free_guidance,
negative_prompt=negative_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
)

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:
Img2img validates that precomputed prompt_embeds must be accompanied by pooled embeds, but then does not pass pooled_prompt_embeds or negative_pooled_prompt_embeds into encode_prompt.

Impact:
The public precomputed-embedding path for Kolors img2img is unusable.

Reproduction:

importtorchfromdiffusersimportKolorsImg2ImgPipelineclassDummy(KolorsImg2ImgPipeline):
@propertydef_execution_device(self):
returntorch.device("cpu")
@propertydefdo_classifier_free_guidance(self):
returnTruedefcheck_inputs(self, *args, **kwargs):
passdefencode_prompt(self, **kwargs):
print("pooled passed:", kwargs.get("pooled_prompt_embeds") isnotNone)
print("negative pooled passed:", kwargs.get("negative_pooled_prompt_embeds") isnotNone)
raiseRuntimeError("stop")
pipe=Dummy.__new__(Dummy)
pipe.default_sample_size=8pipe.vae_scale_factor=8try:
pipe(
prompt=None,
image=torch.zeros(1, 3, 64, 64),
prompt_embeds=torch.randn(1, 4, 8),
pooled_prompt_embeds=torch.randn(1, 8),
negative_prompt_embeds=torch.randn(1, 4, 8),
negative_pooled_prompt_embeds=torch.randn(1, 8),
)
exceptRuntimeError:
pass# Both printed values are False.

Relevant precedent:
KolorsPipeline.__call__ passes both pooled tensors:

) =self.encode_prompt(
prompt=prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
do_classifier_free_guidance=self.do_classifier_free_guidance,
negative_prompt=negative_prompt,
prompt_embeds=prompt_embeds,
pooled_prompt_embeds=pooled_prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
)

Suggested fix:

) =self.encode_prompt(
...
prompt_embeds=prompt_embeds,
pooled_prompt_embeds=pooled_prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
max_sequence_length=max_sequence_length,
)

Issue 6: Img2img keeps stale SDXL-only offload and LoRA assumptions

Affected code:

from ...loadersimportIPAdapterMixin, StableDiffusionXLLoraLoaderMixin
from ...modelsimportAutoencoderKL, ImageProjection, UNet2DConditionModel

classKolorsImg2ImgPipeline(DiffusionPipeline, StableDiffusionMixin, StableDiffusionXLLoraLoaderMixin, IPAdapterMixin):
r"""
Pipeline for text-to-image generation using Kolors.
This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods the
library implements for all the pipelines (such as downloading or saving, running on a particular device, etc.)
The pipeline also inherits the following loading methods:
- [`~loaders.StableDiffusionXLLoraLoaderMixin.load_lora_weights`] for loading LoRA weights
- [`~loaders.StableDiffusionXLLoraLoaderMixin.save_lora_weights`] for saving LoRA weights

model_cpu_offload_seq="text_encoder->image_encoder-unet->vae"

# Offload text encoder if `enable_model_cpu_offload` was enabled
ifhasattr(self, "final_offload_hook") andself.final_offload_hookisnotNone:
self.text_encoder_2.to("cpu")
empty_device_cache()

Problem:
model_cpu_offload_seq contains "image_encoder-unet" instead of "image_encoder->unet". prepare_latents references nonexistent self.text_encoder_2. The class also inherits StableDiffusionXLLoraLoaderMixin, whose loadable modules include text_encoder_2, while Kolors has only one text encoder.

Impact:
Model CPU offload is not chained correctly for img2img, stale offload-hook paths crash, and img2img LoRA loading inherits the same two-text-encoder assumption that PR #11198 fixed for the base Kolors pipeline.

Reproduction:

importtorchfromtypesimportSimpleNamespacefromdiffusersimportKolorsImg2ImgPipelineprint(KolorsImg2ImgPipeline.model_cpu_offload_seq.split("->"))
print(KolorsImg2ImgPipeline._lora_loadable_modules)
pipe=KolorsImg2ImgPipeline.__new__(KolorsImg2ImgPipeline)
pipe.vae=SimpleNamespace(config=SimpleNamespace(latents_mean=None, latents_std=None))
pipe.final_offload_hook=object()
try:
pipe.prepare_latents(torch.zeros(1, 3, 8, 8), torch.tensor([1]), 1, 1, torch.float32, torch.device("cpu"))
exceptExceptionase:
print(type(e).__name__, e)

Relevant precedent:
KolorsPipeline already uses StableDiffusionLoraLoaderMixin and the correct offload sequence:

from ...loadersimportIPAdapterMixin, StableDiffusionLoraLoaderMixin
from ...modelsimportAutoencoderKL, ImageProjection, UNet2DConditionModel

model_cpu_offload_seq="text_encoder->image_encoder->unet->vae"

Suggested fix:

from ...loadersimportIPAdapterMixin, StableDiffusionLoraLoaderMixinclassKolorsImg2ImgPipeline(DiffusionPipeline, StableDiffusionMixin, StableDiffusionLoraLoaderMixin, IPAdapterMixin):
model_cpu_offload_seq="text_encoder->image_encoder->unet->vae"# In prepare_latents:self.text_encoder.to("cpu")

Issue 7: ChatGLMConfig() lacks the original_rope default required by ChatGLMModel

Affected code:

classChatGLMConfig(PretrainedConfig):
model_type="chatglm"
def__init__(
self,
num_layers=28,
padded_vocab_size=65024,
hidden_size=4096,
ffn_hidden_size=13696,
kv_channels=128,
num_attention_heads=32,
seq_length=2048,
hidden_dropout=0.0,
classifier_dropout=None,
attention_dropout=0.0,
layernorm_epsilon=1e-5,
rmsnorm=True,
apply_residual_connection_post_layernorm=False,
post_layer_norm=True,
add_bias_linear=False,
add_qkv_bias=False,
bias_dropout_fusion=True,
multi_query_attention=False,
multi_query_group_num=1,
apply_query_key_layer_scaling=True,
attention_softmax_in_fp32=True,
fp32_residual_connection=False,
quantization_bit=0,
pre_seq_len=None,
prefix_projection=False,
**kwargs,
):
self.num_layers=num_layers
self.vocab_size=padded_vocab_size
self.padded_vocab_size=padded_vocab_size
self.hidden_size=hidden_size
self.ffn_hidden_size=ffn_hidden_size
self.kv_channels=kv_channels
self.num_attention_heads=num_attention_heads
self.seq_length=seq_length
self.hidden_dropout=hidden_dropout
self.classifier_dropout=classifier_dropout
self.attention_dropout=attention_dropout
self.layernorm_epsilon=layernorm_epsilon
self.rmsnorm=rmsnorm

rotary_dim= (
config.hidden_size//config.num_attention_headsifconfig.kv_channelsisNoneelseconfig.kv_channels
)
self.rotary_pos_emb=RotaryEmbedding(rotary_dim//2, original_impl=config.original_rope, device=device)
self.encoder=init_method(GLMTransformer, config, **init_kwargs)

Problem:
ChatGLMModel.__init__ reads config.original_rope, but ChatGLMConfig.__init__ never defines it unless it arrives via pretrained-config kwargs.

Impact:
A fresh/synthetic ChatGLMConfig cannot instantiate ChatGLMModel, which breaks local tiny configs and normal config round-tripping expectations.

Reproduction:

fromdiffusers.pipelines.kolors.text_encoderimportChatGLMConfig, ChatGLMModelcfg=ChatGLMConfig(
num_layers=1,
hidden_size=8,
ffn_hidden_size=16,
kv_channels=4,
num_attention_heads=2,
padded_vocab_size=32,
seq_length=8,
)
ChatGLMModel(cfg, empty_init=False)
# AttributeError: 'ChatGLMConfig' object has no attribute 'original_rope'

Relevant precedent:
The tiny pretrained ChatGLM config works only because its remote config includes original_rope=True; the class default should still be self-contained.

Suggested fix:

def__init__(..., prefix_projection=False, original_rope=False, **kwargs):
...
self.prefix_projection=prefix_projectionself.original_rope=original_ropesuper().__init__(**kwargs)

Issue 8: Slow tests are missing for Kolors

Affected code:

classKolorsPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
pipeline_class=KolorsPipeline
params=TEXT_TO_IMAGE_PARAMS
batch_params=TEXT_TO_IMAGE_BATCH_PARAMS
image_params=TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params=TEXT_TO_IMAGE_IMAGE_PARAMS
callback_cfg_params=TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
supports_dduf=False
test_layerwise_casting=True
defget_dummy_components(self, time_cond_proj_dim=None):
torch.manual_seed(0)
unet=UNet2DConditionModel(
block_out_channels=(2, 4),
layers_per_block=2,
time_cond_proj_dim=time_cond_proj_dim,
sample_size=32,
in_channels=4,
out_channels=4,
down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"),
up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"),
# specific config below
attention_head_dim=(2, 4),
use_linear_projection=True,
addition_embed_type="text_time",
addition_time_embed_dim=8,
transformer_layers_per_block=(1, 2),
projection_class_embeddings_input_dim=56,
cross_attention_dim=8,
norm_num_groups=1,
)
scheduler=EulerDiscreteScheduler(
beta_start=0.00085,
beta_end=0.012,
steps_offset=1,
beta_schedule="scaled_linear",
timestep_spacing="leading",
)
torch.manual_seed(0)
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=128,
)
torch.manual_seed(0)
text_encoder=ChatGLMModel.from_pretrained(
"hf-internal-testing/tiny-random-chatglm3-6b", torch_dtype=torch.float32
)
tokenizer=ChatGLMTokenizer.from_pretrained("hf-internal-testing/tiny-random-chatglm3-6b")
components= {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
"image_encoder": None,
"feature_extractor": None,
}
returncomponents
defget_dummy_inputs(self, device, seed=0):
ifstr(device).startswith("mps"):
generator=torch.manual_seed(seed)
else:
generator=torch.Generator(device=device).manual_seed(seed)
inputs= {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"num_inference_steps": 2,
"guidance_scale": 5.0,
"output_type": "np",
}
returninputs
deftest_inference(self):
device="cpu"
components=self.get_dummy_components()
pipe=self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
inputs=self.get_dummy_inputs(device)
image=pipe(**inputs).images
image_slice=image[0, -3:, -3:, -1]

classKolorsPipelineImg2ImgFastTests(PipelineTesterMixin, unittest.TestCase):
pipeline_class=KolorsImg2ImgPipeline
params=TEXT_TO_IMAGE_PARAMS
batch_params=TEXT_TO_IMAGE_BATCH_PARAMS
image_params=TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params=TEXT_TO_IMAGE_IMAGE_PARAMS
callback_cfg_params=TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
supports_dduf=False
# Copied from tests.pipelines.kolors.test_kolors.KolorsPipelineFastTests.get_dummy_components
defget_dummy_components(self, time_cond_proj_dim=None):
torch.manual_seed(0)
unet=UNet2DConditionModel(
block_out_channels=(2, 4),
layers_per_block=2,
time_cond_proj_dim=time_cond_proj_dim,
sample_size=32,
in_channels=4,
out_channels=4,
down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"),
up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"),
# specific config below
attention_head_dim=(2, 4),
use_linear_projection=True,
addition_embed_type="text_time",
addition_time_embed_dim=8,
transformer_layers_per_block=(1, 2),
projection_class_embeddings_input_dim=56,
cross_attention_dim=8,
norm_num_groups=1,
)
scheduler=EulerDiscreteScheduler(
beta_start=0.00085,
beta_end=0.012,
steps_offset=1,
beta_schedule="scaled_linear",
timestep_spacing="leading",
)
torch.manual_seed(0)
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=128,
)
torch.manual_seed(0)
text_encoder=ChatGLMModel.from_pretrained(
"hf-internal-testing/tiny-random-chatglm3-6b", torch_dtype=torch.float32
)
tokenizer=ChatGLMTokenizer.from_pretrained("hf-internal-testing/tiny-random-chatglm3-6b")
components= {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
"image_encoder": None,
"feature_extractor": None,
}
returncomponents
defget_dummy_inputs(self, device, seed=0):
image=floats_tensor((1, 3, 64, 64), rng=random.Random(seed)).to(device)
image=image/2+0.5
ifstr(device).startswith("mps"):
generator=torch.manual_seed(seed)
else:
generator=torch.Generator(device=device).manual_seed(seed)
inputs= {
"prompt": "A painting of a squirrel eating a burger",
"image": image,
"generator": generator,
"num_inference_steps": 2,
"guidance_scale": 5.0,
"output_type": "np",
"strength": 0.8,
}
returninputs
deftest_inference(self):
device="cpu"
components=self.get_dummy_components()
pipe=self.pipeline_class(**components)
pipe.to(device)
pipe.set_progress_bar_config(disable=None)
inputs=self.get_dummy_inputs(device)
image=pipe(**inputs).images
image_slice=image[0, -3:, -3:, -1]
self.assertEqual(image.shape, (1, 64, 64, 3))
expected_slice=np.array(
[0.54823864, 0.43654007, 0.4886489, 0.63072854, 0.53641886, 0.4896852, 0.62123513, 0.5621531, 0.42809626]
)
max_diff=np.abs(image_slice.flatten() -expected_slice).max()
self.assertLessEqual(max_diff, 1e-3)
deftest_inference_batch_single_identical(self):
self._test_inference_batch_single_identical(batch_size=3, expected_max_diff=3e-3)
deftest_float16_inference(self):
super().test_float16_inference(expected_max_diff=7e-2)
@unittest.skip("Test not supported because kolors img2img doesn't take pooled embeds as inputs unlike kolors t2i.")

classKolorsPAGPipelineFastTests(
PipelineTesterMixin,
PipelineFromPipeTesterMixin,
unittest.TestCase,
):
pipeline_class=KolorsPAGPipeline
params=TEXT_TO_IMAGE_PARAMS.union({"pag_scale", "pag_adaptive_scale"})
batch_params=TEXT_TO_IMAGE_BATCH_PARAMS
image_params=TEXT_TO_IMAGE_IMAGE_PARAMS
image_latents_params=TEXT_TO_IMAGE_IMAGE_PARAMS
callback_cfg_params=TEXT_TO_IMAGE_CALLBACK_CFG_PARAMS.union({"add_text_embeds", "add_time_ids"})
supports_dduf=False
# Copied from tests.pipelines.kolors.test_kolors.KolorsPipelineFastTests.get_dummy_components
defget_dummy_components(self, time_cond_proj_dim=None):
torch.manual_seed(0)
unet=UNet2DConditionModel(
block_out_channels=(2, 4),
layers_per_block=2,
time_cond_proj_dim=time_cond_proj_dim,
sample_size=32,
in_channels=4,
out_channels=4,
down_block_types=("DownBlock2D", "CrossAttnDownBlock2D"),
up_block_types=("CrossAttnUpBlock2D", "UpBlock2D"),
# specific config below
attention_head_dim=(2, 4),
use_linear_projection=True,
addition_embed_type="text_time",
addition_time_embed_dim=8,
transformer_layers_per_block=(1, 2),
projection_class_embeddings_input_dim=56,
cross_attention_dim=8,
norm_num_groups=1,
)
scheduler=EulerDiscreteScheduler(
beta_start=0.00085,
beta_end=0.012,
steps_offset=1,
beta_schedule="scaled_linear",
timestep_spacing="leading",
)
torch.manual_seed(0)
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=128,
)
torch.manual_seed(0)
text_encoder=ChatGLMModel.from_pretrained(
"hf-internal-testing/tiny-random-chatglm3-6b", torch_dtype=torch.float32
)
tokenizer=ChatGLMTokenizer.from_pretrained("hf-internal-testing/tiny-random-chatglm3-6b")
components= {
"unet": unet,
"scheduler": scheduler,
"vae": vae,
"text_encoder": text_encoder,
"tokenizer": tokenizer,
"image_encoder": None,
"feature_extractor": None,
}
returncomponents
defget_dummy_inputs(self, device, seed=0):
ifstr(device).startswith("mps"):
generator=torch.manual_seed(seed)
else:
generator=torch.Generator(device=device).manual_seed(seed)
inputs= {
"prompt": "A painting of a squirrel eating a burger",
"generator": generator,
"num_inference_steps": 2,
"guidance_scale": 5.0,
"pag_scale": 0.9,
"output_type": "np",
}
returninputs
deftest_pag_disable_enable(self):
device="cpu"# ensure determinism for the device-dependent torch.Generator
components=self.get_dummy_components()
# base pipeline (expect same output when pag is disabled)
pipe_sd=KolorsPipeline(**components)
pipe_sd=pipe_sd.to(device)
pipe_sd.set_progress_bar_config(disable=None)
inputs=self.get_dummy_inputs(device)
delinputs["pag_scale"]
assert"pag_scale"notininspect.signature(pipe_sd.__call__).parameters, (
f"`pag_scale` should not be a call parameter of the base pipeline {pipe_sd.__class__.__name__}."
)
out=pipe_sd(**inputs).images[0, -3:, -3:, -1]
# pag disabled with pag_scale=0.0
pipe_pag=self.pipeline_class(**components)
pipe_pag=pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs=self.get_dummy_inputs(device)
inputs["pag_scale"] =0.0
out_pag_disabled=pipe_pag(**inputs).images[0, -3:, -3:, -1]
# pag enabled
pipe_pag=self.pipeline_class(**components, pag_applied_layers=["mid", "up", "down"])
pipe_pag=pipe_pag.to(device)
pipe_pag.set_progress_bar_config(disable=None)
inputs=self.get_dummy_inputs(device)
out_pag_enabled=pipe_pag(**inputs).images[0, -3:, -3:, -1]
assertnp.abs(out.flatten() -out_pag_disabled.flatten()).max() <1e-3
assertnp.abs(out.flatten() -out_pag_enabled.flatten()).max() >1e-3
deftest_pag_applied_layers(self):
device="cpu"# ensure determinism for the device-dependent torch.Generator
components=self.get_dummy_components()
# base pipeline
pipe=self.pipeline_class(**components)
pipe=pipe.to(device)
pipe.set_progress_bar_config(disable=None)
# pag_applied_layers = ["mid","up","down"] should apply to all self-attention layers
all_self_attn_layers= [kforkinpipe.unet.attn_processors.keys() if"attn1"ink]
original_attn_procs=pipe.unet.attn_processors
pag_layers= ["mid", "down", "up"]

Problem:
Kolors has fast tests using hf-internal-testing/tiny-random-chatglm3-6b, but no @slow tests for the real Kwai-Kolors/Kolors-diffusers text2img/img2img behavior. The prompt explicitly requires missing slow tests to be reported.

Impact:
Real-checkpoint regressions in prompt encoding, scheduler defaults, dtype/device behavior, offload, IP-Adapter, and img2img conditioning can ship without coverage.

Reproduction:

frompathlibimportPathpaths=list(Path("tests/pipelines/kolors").glob("test_*.py")) + [Path("tests/pipelines/pag/test_pag_kolors.py")]
missing= [p.as_posix() forpinpathsif"@slow"notinp.read_text(encoding="utf-8")]
print("\n".join(missing))

Relevant precedent:
Many mature pipeline families include both tiny fast tests and at least one real-checkpoint slow smoke test for main workflows.

Suggested fix:
Add @slow smoke tests for KolorsPipeline and KolorsImg2ImgPipeline using Kwai-Kolors/Kolors-diffusers with a small deterministic prompt/image, low steps, and either a numerical slice or shape/sanity assertion. Include an offload/IP-Adapter slow path if runtime budget allows.

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