huggingface / huggingface/diffusers

nucleusmoe_image model/pipeline review

Open
#13,590 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
34.5k
Forks
7.3k
Avg merge
3d 3h
Merged PRs (30d)
91

Description

nucleusmoe_image model/pipeline review

Commit tested: 0f1abc4ae8b0eb2a3b40e82a310507281144c423

Review performed against the repository review rules.

Issue 1: Batched prompts can crash CFG when negative_prompt is a string

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/nucleusmoe_image/pipeline_nucleusmoe_image.py#L492-L518
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_nucleusmoe_image.py#L317-L318

Problem:
For prompt=["a", "b"], batch_size is 2. If the caller passes negative_prompt="bad" with guidance_scale > 1, the negative prompt is encoded as batch size 1, then passed to the transformer with latents of batch size 2. The attention processor later concatenates image and text K/V tensors and raises a batch-size mismatch.

Impact:
Common batched text-to-image usage crashes during CFG instead of either broadcasting the scalar negative prompt or raising a clear validation error.

Reproduction:

import torch
from diffusers import NucleusMoEImageTransformer2DModel

model = NucleusMoEImageTransformer2DModel(
    patch_size=2, in_channels=16, out_channels=4, num_layers=1,
    attention_head_dim=16, num_attention_heads=4, joint_attention_dim=16,
    axes_dims_rope=(8, 4, 4), moe_enabled=False, capacity_factors=[8.0],
).eval()

latents = torch.randn(2, 16, 16)          # prompt=["a", "b"]
negative_embeds = torch.randn(1, 8, 16)  # negative_prompt="bad"

with torch.no_grad():
    model(
        hidden_states=latents,
        timestep=torch.ones(2),
        encoder_hidden_states=negative_embeds,
        encoder_hidden_states_mask=torch.ones(1, 8, dtype=torch.long),
        img_shapes=[(1, 4, 4), (1, 4, 4)],
    )

Relevant precedent:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py#L744-L748
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py#L455-L472

Suggested fix:

if do_cfg and negative_prompt_embeds is None:
    if negative_prompt is None:
        negative_prompt = [""] * batch_size
    elif isinstance(negative_prompt, str) and batch_size > 1:
        negative_prompt = [negative_prompt] * batch_size
    elif isinstance(negative_prompt, list) and len(negative_prompt) != batch_size:
        raise ValueError(
            f"`negative_prompt` has batch size {len(negative_prompt)}, but `prompt` has batch size {batch_size}."
        )

Issue 2: return_index rejects valid hidden-state indices and ignores 0

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/nucleusmoe_image/pipeline_nucleusmoe_image.py#L214-L235
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/nucleusmoe_image/pipeline_nucleusmoe_image.py#L296-L300

Problem:
return_index = return_index or self.default_return_index makes explicit return_index=0 impossible. Validation also uses abs(return_index) >= num_hidden_layers, but Transformer hidden states include the embeddings plus one entry per layer, so valid indices include num_hidden_layers and -(num_hidden_layers + 1) through -1.

Impact:
Users cannot select hidden state 0, and valid negative layer selections can be rejected before encoding.

Reproduction:

from types import SimpleNamespace
import torch
from diffusers import NucleusMoEImagePipeline

class Batch(dict):
    @property
    def attention_mask(self):
        return self["attention_mask"]
    def to(self, device=None):
        return self

class FakeProcessor:
    def apply_chat_template(self, *args, **kwargs):
        return "formatted"
    def __call__(self, **kwargs):
        return Batch(input_ids=torch.tensor([[1]]), attention_mask=torch.tensor([[1]]))

class FakeTextEncoder:
    dtype = torch.float32
    config = SimpleNamespace(text_config=SimpleNamespace(num_hidden_layers=8))
    def __call__(self, **kwargs):
        return SimpleNamespace(hidden_states=[torch.full((1, 1, 1), i, dtype=torch.float32) for i in range(9)])

pipe = object.__new__(NucleusMoEImagePipeline)
pipe.processor = FakeProcessor()
pipe.text_encoder = FakeTextEncoder()
pipe.default_return_index = -8

embeds, _ = pipe.encode_prompt("x", device=torch.device("cpu"), return_index=0)
print(embeds.item())  # 1.0, expected 0.0

pipe.vae_scale_factor = 8
pipe._callback_tensor_inputs = ["latents", "prompt_embeds"]
pipe.check_inputs("x", 64, 64, return_index=-8)  # raises, but hidden_states[-8] is valid for length 9

Suggested fix:

return_index = self.default_return_index if return_index is None else return_index

num_hidden_states = self.text_encoder.config.text_config.num_hidden_layers + 1
if return_index is not None and not (-num_hidden_states <= return_index < num_hidden_states):
    raise ValueError(
        f"`return_index` must be in [{-num_hidden_states}, {num_hidden_states - 1}], but is {return_index}."
    )

Issue 3: Precomputed prompt embeddings ignore max_sequence_length

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/nucleusmoe_image/pipeline_nucleusmoe_image.py#L236-L249

Problem:
When prompt_embeds are supplied directly, encode_prompt moves them to the device but never slices prompt_embeds or prompt_embeds_mask to max_sequence_length. Freshly encoded prompts are truncated by the processor, so the two code paths disagree.

Impact:
Calls that use cached/precomputed embeddings can feed longer text context than requested, changing RoPE text positions, attention length, memory use, and output parity with the normal prompt path.

Reproduction:

import torch
from diffusers import NucleusMoEImagePipeline

pipe = object.__new__(NucleusMoEImagePipeline)
pipe.default_return_index = -8

embeds, mask = pipe.encode_prompt(
    prompt_embeds=torch.randn(1, 8, 4),
    prompt_embeds_mask=torch.tensor([[1, 1, 1, 1, 0, 0, 0, 0]]),
    device=torch.device("cpu"),
    max_sequence_length=4,
)
print(embeds.shape, mask.shape)  # torch.Size([1, 8, 4]) torch.Size([1, 8])

Relevant precedent:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/qwenimage/pipeline_qwenimage.py#L256-L264

Suggested fix:

if max_sequence_length is None:
    max_sequence_length = self.default_max_sequence_length

prompt_embeds = prompt_embeds[:, :max_sequence_length]
if prompt_embeds_mask is not None:
    prompt_embeds_mask = prompt_embeds_mask[:, :max_sequence_length]

Issue 4: Generic Attention APIs can replace the custom processor and break NucleusMoE attention

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_nucleusmoe_image.py#L643-L657

Problem:
The block instantiates the generic Attention class with NucleusMoEAttnProcessor2_0. Generic Attention exposes processor-changing APIs such as set_attention_slice, which replace the Nucleus processor with incompatible generic processors. The review rules require a model-local attention class using AttentionModuleMixin, _default_processor_cls, and _available_processors to avoid exactly this class of breakage.

Impact:
Lower-level attention APIs can leave the model in a broken state. The pipeline fast test for attention slicing does not catch this because DiffusionPipeline.enable_attention_slicing() only calls components that expose set_attention_slice; NucleusMoEImageTransformer2DModel does not, so the pipeline-level call is effectively a no-op.

Reproduction:

import torch
from diffusers import NucleusMoEImageTransformer2DModel

model = NucleusMoEImageTransformer2DModel(
    patch_size=2, in_channels=16, out_channels=4, num_layers=1,
    attention_head_dim=16, num_attention_heads=4, joint_attention_dim=16,
    axes_dims_rope=(8, 4, 4), moe_enabled=False, capacity_factors=[8.0],
).eval()

model.transformer_blocks[0].attn.set_attention_slice(1)

with torch.no_grad():
    model(
        hidden_states=torch.randn(1, 16, 16),
        timestep=torch.ones(1),
        encoder_hidden_states=torch.randn(1, 8, 16),
        encoder_hidden_states_mask=torch.ones(1, 8, dtype=torch.long),
        img_shapes=[(1, 4, 4)],
    )

Relevant precedent:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_flux2.py#L493-L548
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/models/transformers/transformer_wan.py#L175-L177

Suggested fix:
Implement a NucleusMoEAttention(torch.nn.Module, AttentionModuleMixin) in transformer_nucleusmoe_image.py, move the projections/norms used by NucleusMoEAttnProcessor2_0 into it, set _default_processor_cls = NucleusMoEAttnProcessor2_0, and restrict _available_processors to compatible processors.

Issue 5: Slow tests are missing

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/pipelines/nucleusmoe_image/test_nucleusmoe_image.py#L38-L337
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/tests/models/transformers/test_models_transformer_nucleusmoe_image.py#L104-L220

Problem:
The target family has fast pipeline and model tests, but no @slow coverage for the real published checkpoint.

Impact:
The integration does not verify real-component loading, real tokenizer/processor behavior, checkpoint config compatibility, or an expected output slice. This leaves conversion/parity regressions undetected.

Reproduction:

from pathlib import Path

for path in sorted(Path("tests").rglob("*nucleusmoe*")):
    if path.is_file() and "__pycache__" not in path.parts:
        text = path.read_text(encoding="utf-8", errors="ignore")
        print(path, "@slow" in text)

Suggested fix:
Add a slow pipeline test that loads NucleusAI/NucleusMoE-Image with the documented dtype/device path, runs a small deterministic prompt, and checks an output slice or image statistics. If full generation is too expensive, add a slow load/encode/one-step smoke test with explicit justification.

Issue 6: Public pipeline has no docs or examples

Affected code:
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/auto_pipeline.py#L80
https://github.com/huggingface/diffusers/blob/0f1abc4ae8b0eb2a3b40e82a310507281144c423/src/diffusers/pipelines/auto_pipeline.py#L183

Problem:
NucleusMoEImagePipeline is exported and registered in auto-pipeline mappings, but there are no matching docs or examples under docs/ or examples/.

Impact:
The public pipeline lacks discoverable API documentation, output docs, usage notes for Qwen3-VL processor requirements, dtype/offload recommendations, and any documented limitations.

Reproduction:

from pathlib import Path

patterns = ("NucleusMoE", "nucleusmoe", "NucleusMoEImage")
for root in [Path("docs"), Path("examples")]:
    hits = []
    for path in root.rglob("*"):
        if path.is_file() and path.suffix.lower() in {".md", ".mdx", ".py", ".rst"}:
            text = path.read_text(encoding="utf-8", errors="ignore")
            if any(pattern in text for pattern in patterns):
                hits.append(str(path))
    print(root, hits)

Suggested fix:
Add a docs page, include it in the pipeline docs toctree, document NucleusMoEImagePipeline, NucleusMoEImagePipelineOutput, usage with NucleusAI/NucleusMoE-Image, and recommended memory/offload settings.

Duplicate-search status

Searched GitHub Issues and PRs in huggingface/diffusers for NucleusMoEImage, NucleusMoEImagePipeline, NucleusMoEImageTransformer2DModel, nucleusmoe_image, pipeline_nucleusmoe_image, return_index, negative_prompt, prompt_embeds max_sequence_length, slow tests, and docs. No duplicate issue/PR was found for these findings. The only related PR surfaced was the merged integration PR: https://github.com/huggingface/diffusers/pull/13317.

Verification notes: top-level imports work in the current .venv, and a tiny transformer save/load round trip produced max diff 0.0. Targeted pytest collection failed in this .venv before running tests because the installed Windows PyTorch build lacks torch._C._distributed_c10d.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/diffusers/pipelines/nucleusmoe_image/pipeline_nucleusmoe_image.py and src/diffusers/models/transformers/transformer_nucleusmoe_image.py, then reproduce the listed prompt, return_index, embedding, and attention cases. Review the fast tests in tests/pipelines/nucleusmoe_image/test_nucleusmoe_image.py and tests/models/transformers/test_models_transformer_nucleusmoe_image.py, plus the cited attention precedents. Done means the reported cases are covered, slow checkpoint coverage exists, and the public pipeline has docs and examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
documentation, machine-learning, testing-qa
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.