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_importRelevant 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.
kolorsmodel/pipeline reviewCommit tested:
0f1abc4ae8b0eb2a3b40e82a310507281144c423Review 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, ororiginal_ropefindings below.Test note: targeted reproductions ran under
.venv. Full fast-test collection withpython -m pytest tests/pipelines/kolors/test_kolors.py tests/pipelines/kolors/test_kolors_img2img.py -qfailed in this local environment because the installed torch build lackstorch._C._distributed_c10d.Issue 1:
KolorsPipelineOutputis not exported fromdiffusers.pipelines.kolorsAffected code:
diffusers/src/diffusers/pipelines/kolors/__init__.py
Lines 15 to 28 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_output.py
Lines 10 to 20 in 0f1abc4
Problem:
KolorsPipelineOutputis defined and referenced in docstrings as~pipelines.kolors.KolorsPipelineOutput, butkolors/__init__.pynever addspipeline_outputto_import_structure.Impact:
Public subpackage import fails and autodoc cross-references can resolve inconsistently.
Reproduction:
Relevant precedent:
stable_diffusion,stable_diffusion_xl,qwenimage, andfluxexport their pipeline output classes from the subpackage__init__.py.Suggested fix:
Issue 2: Kolors subpackage lazy import still breaks when
sentencepieceis missingAffected code:
diffusers/src/diffusers/pipelines/kolors/__init__.py
Lines 18 to 28 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/__init__.py
Lines 32 to 39 in 0f1abc4
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 importsentencepiece.Impact:
Direct imports from
diffusers.pipelines.kolorscan 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:
Relevant precedent:
The parent package has the correct condition:
diffusers/src/diffusers/pipelines/__init__.py
Lines 473 to 484 in 0f1abc4
Suggested fix:
Issue 3:
max_sequence_lengthis validated but ignored by both pipeline callsAffected code:
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors.py
Lines 865 to 875 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 1018 to 1026 in 0f1abc4
Problem:
__call__accepts and validatesmax_sequence_length, but does not pass it intoencode_prompt, so tokenization always uses the default256.Impact:
Users cannot shorten prompt encoding for memory/performance or test non-default sequence lengths through the public pipeline API.
Reproduction:
Relevant precedent:
QwenImage forwards the call-time value:
diffusers/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py
Lines 585 to 630 in 0f1abc4
Suggested fix:
Issue 4:
encode_promptmishandles zeroed negatives and precomputed prompt embedsAffected code:
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors.py
Lines 289 to 292 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors.py
Lines 351 to 359 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 309 to 312 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 371 to 379 in 0f1abc4
Problem:
When
force_zeros_for_empty_prompt=True, the code zerosnegative_prompt_embedsbut leavesnegative_pooled_prompt_embeds=None, then calls.repeat(...). Separately, if users pass precomputedprompt_embedsandnegative_prompt_embeds, only pooled embeds are repeated fornum_images_per_prompt; sequence embeds stay at batch size 1.Impact:
The zero-negative config path crashes. Precomputed embeddings with
num_images_per_prompt > 1later fail in UNet attention because latent batch and text batch do not match.Reproduction:
Relevant precedent:
SDXL zeros the pooled negative embed and always duplicates prompt embeds after encoding/reuse:
diffusers/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py
Lines 421 to 499 in 0f1abc4
Suggested fix:
Apply in
pipeline_kolors.py, then propagate copied blocks.Issue 5:
KolorsImg2ImgPipeline.__call__drops pooled prompt embedsAffected code:
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 1018 to 1026 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 534 to 541 in 0f1abc4
Problem:
Img2img validates that precomputed
prompt_embedsmust be accompanied by pooled embeds, but then does not passpooled_prompt_embedsornegative_pooled_prompt_embedsintoencode_prompt.Impact:
The public precomputed-embedding path for Kolors img2img is unusable.
Reproduction:
Relevant precedent:
KolorsPipeline.__call__passes both pooled tensors:diffusers/src/diffusers/pipelines/kolors/pipeline_kolors.py
Lines 865 to 875 in 0f1abc4
Suggested fix:
Issue 6: Img2img keeps stale SDXL-only offload and LoRA assumptions
Affected code:
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 23 to 24 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 142 to 151 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Line 171 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors_img2img.py
Lines 617 to 620 in 0f1abc4
Problem:
model_cpu_offload_seqcontains"image_encoder-unet"instead of"image_encoder->unet".prepare_latentsreferences nonexistentself.text_encoder_2. The class also inheritsStableDiffusionXLLoraLoaderMixin, whose loadable modules includetext_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:
Relevant precedent:
KolorsPipelinealready usesStableDiffusionLoraLoaderMixinand the correct offload sequence:diffusers/src/diffusers/pipelines/kolors/pipeline_kolors.py
Lines 22 to 23 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/pipeline_kolors.py
Line 152 in 0f1abc4
Suggested fix:
Issue 7:
ChatGLMConfig()lacks theoriginal_ropedefault required byChatGLMModelAffected code:
diffusers/src/diffusers/pipelines/kolors/text_encoder.py
Lines 31 to 75 in 0f1abc4
diffusers/src/diffusers/pipelines/kolors/text_encoder.py
Lines 762 to 767 in 0f1abc4
Problem:
ChatGLMModel.__init__readsconfig.original_rope, butChatGLMConfig.__init__never defines it unless it arrives via pretrained-config kwargs.Impact:
A fresh/synthetic
ChatGLMConfigcannot instantiateChatGLMModel, which breaks local tiny configs and normal config round-tripping expectations.Reproduction:
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:
Issue 8: Slow tests are missing for Kolors
Affected code:
diffusers/tests/pipelines/kolors/test_kolors.py
Lines 42 to 132 in 0f1abc4
diffusers/tests/pipelines/kolors/test_kolors_img2img.py
Lines 46 to 158 in 0f1abc4
diffusers/tests/pipelines/pag/test_pag_kolors.py
Lines 47 to 180 in 0f1abc4
Problem:
Kolors has fast tests using
hf-internal-testing/tiny-random-chatglm3-6b, but no@slowtests for the realKwai-Kolors/Kolors-diffuserstext2img/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:
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
@slowsmoke tests forKolorsPipelineandKolorsImg2ImgPipelineusingKwai-Kolors/Kolors-diffuserswith 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.