Skip to content

[WIP] Refactor Model Design - #13794

Open
DN6 wants to merge 9 commits into
mainfrom
refactor-model-metadata
Open

DN6 wants to merge 9 commits into
mainfrom
refactor-model-metadata

Conversation

@DN6

@DN6 DN6 commented May 22, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

This refactor turns models into self-contained modules that declare their capabilities in one place. Per-model conversion code moves next to the model and a unified metadata() API makes feature attributes inspectable from any model class.

Motivation

Today, features are added to models through a mix of class attributes and mixins. Mixins define their own class attributes as well, so when examining a model class it isn't immediately clear which attributes and features are relevant or available.

Models are defined in a single file, so we end up using centralized utility files for things like model-specific weight and LoRA conversions. These files have grown enormous as they accumulate code to handle per-model variants and their idiosyncrasies.

The new design makes the mixins model-agnostic and has each mixin reach for the per-model metadata it needs through small handler objects attached to the model class.

Proposed Structure

Using Flux as a reference:

models/transformers/flux/
├── __init__.py
├── _ip_adapter.py        # FluxIPAdapterMixin + converters (internal)
├── _lora.py              # FLUX_LORA handler + per-format converters (internal)
├── _weight_mapping.py    # FLUX_WEIGHT_MAPPING handler + key tables (internal)
└── model.py              # FluxTransformer2DModel class declaration

Two patterns live next to model.py, picked per subsystem based on whether the behavior actually generalizes across models:

  1. handler + shared mixin — for features where the steps are the same across models and only the data/conversion function varies. The model opts in by inheriting the shared mixin in loaders/ and assigning its handler as a class attribute. LoRA and single-file weight mapping fit here:

    class FluxTransformer2DModel(ModelMixin, LoRAModelMixin, ...):
        _lora = LoRAHandler("...")               # handler instance, consumed by LoRAModelMixin
  2. Per-model mixin — for features that vary too much across models for a single shared mixin to be useful. Each model gets its own mixin declared right next to the model and inherited directly. IP-Adapter is the showcase:

    class FluxTransformer2DModel(..., FluxIPAdapterMixin):
        ...

This should simplify developing on top of these models — modifications or enhancements stay within one folder. If a model has a very specific feature that doesn't generalize across others, it can be kept isolated there too (e.g. FreeNoise for the AnimateDiff UNet). Additionally, if a custom model is modifying an existing diffusers model (Self-Forcing Wan), the folder method of organizing the model lends itself well to custom code loading with AutoModel.

Features Introduced

Model capability introspection via Model.metadata()

Each model exposes a metadata() classmethod that returns a metadata object, keyed by the class attribute that controls each feature. The displayed row tells you exactly what to set or inherit to change the behavior.

>>> print(FluxTransformer2DModel.metadata())
FluxTransformer2DModel feature attributes
──────────────────────────────────────────────────────────────────────────────────
  _supports_gradient_checkpointing  True
  _supports_group_offloading        True
  _no_split_modules                 FluxTransformerBlock, FluxSingleTransformerBlock
  _skip_layerwise_casting_patterns  pos_embed, norm
  _repeated_blocks                  FluxTransformerBlock, FluxSingleTransformerBlock
  _cp_plan                          True
  _weight_mapping                   flux-depth, flux-dev, flux-fill, flux-schnell
  _lora                             bfl, kohya, kontext, xlabs
  _supports_cache                   True
  _supports_ip_adapter              True

The returned ModelMetadata exposes each feature value as an attribute (meta._supports_ip_adapter, meta._lora, ...), supports keys() / values() / items() for mapping-style iteration, and in for presence checks. meta.describe(verbose=True) adds an indented description and docs link under each row. Which can be useful for agents.

Fixes # (issue)

Before submitting

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@DN6
DN6 marked this pull request as ready for review May 27, 2026 16:35
@DN6
DN6 requested review from asomoza, dg845, sayakpaul and yiyixuxu May 27, 2026 16:35

@yiyixuxu yiyixuxu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for working on this!
I really like the direction this is taking, very exciting!
It's a big PR with a lot of changes, so I'll go through it step by step - left some initial questions:)

Comment thread src/diffusers/models/attention.py Outdated
_default_processor_cls = None
_available_processors = []
_supports_qkv_fusion = True
_parallel_config = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need it here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up on this
any reason we need it here? it should live on the attention processor, no?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this proposal should also probably be included in a follow up.

IMO it is better to set attention backend/ CP at the attention module level, rather than at the processor level like we have currently. e.g. If you enable CP and then switch out the processor, you would need to re-enable CP again with a new processor. This feels a bit weird.

  model.enable_parallelism(config)   # writes to current processor
  model.set_processor(NewProcessor()) # NewProcessor._parallel_config defaults back to None
  # CP is silently disabled. The next forward pass runs unsharded.

But we can discuss in a follow up PR. I'll revert for now.

@sayakpaul sayakpaul Jun 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DN6 sorry not to digress but we change between attention backends through set_attention_backend() or with the context manager:

def attention_backend(backend: str | AttentionBackendName = AttentionBackendName.NATIVE):

So, assuming we're doing:

model.enable_parallelism(config) 
model.set_attention_backend(new_backend)

where new_backend is different from the one used during enable_parallelism(), we actually throw an error (if the backend doesn't support CP), see:

if parallel_config_set and not _AttentionBackendRegistry._is_context_parallel_available(backend):
compatible_backends = sorted(_AttentionBackendRegistry._supports_context_parallel)
raise ValueError(
f"Context parallelism is enabled but current attention backend '{backend.value}' "
f"does not support context parallelism. "
f"Please set a compatible attention backend: {compatible_backends} using `model.set_attention_backend()`."
)
_check_attention_backend_requirements(backend)
_maybe_download_kernel_for_backend(backend)

Even the other opposite ordering (set_attention_backend() and enable_parallelism()) checks for compatibility:

raise ValueError(
f"Context parallelism is enabled but the attention processor '{processor.__class__.__name__}' "
f"is using backend '{attention_backend.value}' which does not support context parallelism. "
f"Please set a compatible attention backend: {compatible_backends} using `model.set_attention_backend()` before "
f"calling `model.enable_parallelism()`."
)

IMO, it's better to separate parallelism from attention. On the surface, it may seem like our current parallelism support (CP) is largely operating at the attention level; that may not hold up for long, especially once we ship support for TP.

My points are based on the assumption that we want to encourage users to use set_attention_backend() rather than the attention processor method that you cited above.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sayakpaul I'm not referring to using processors to set attention backends.

Parallelism and attention backends are currently coupled to processors because the _parallel_config and _attention_backend attributes are defined and set on processors.

Processors are swappable and enable different types of attention operations (independent of backend) e.g. for Flux we have FluxIPAdapterAttnProcessor and Flux2 has Flux2KVAttnProcessor . If you were to enable parallelism on Flux and then load an IP Adapter, you would have to enable parallelism again for it to work.

pipeline.transformer.enable_parallelism()
pipeline.load_ip_adapter()
# parallelism is now disabled. 

The _attention_backend attribute is also set at the processor level. We have a fallback that uses the backend set in the global AttentionRegistry if the processor attribute is set to None . But this also a bug that needs fixing and can lead to weird situations such as.

model_A.set_attention_backend("sage_hub")

# e.g. set a new processor to experiment with KV cacheing
model_A.set_processors(...)
model_B.set_attention_backend("flash_hub") # the attention registry global backend is now flash_hub

# this runs with the flash_hub backend because the _attention_backend attribute on the processor is set to None when a new processor is set. 
# It will attempt to use the global backend which is based on the last model that set the backend. 
model_A(**kwargs) 

Moving these attributes into the attention module addresses this issue. But again, I will remove this part from the scope of this PR and introduce a dedicated PR for it. We can discuss where parallelism lives in that PR as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow that is a good point. model_B and model_A shouldn't have this kind of interference!

f"{DOCS_BASE}/optimization/memory#gradient-checkpointing",
)
if cls._supports_group_offloading:
rows["_supports_group_offloading"] = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the metadata + mixin direction but I'd like to understand a bit more: is there a fundamental reason why some features get their own self-contained mixin (Cache, Lora) and many others stay as methods on ModelMixin?

Comment thread src/diffusers/models/modeling_utils.py Outdated
return "\n".join(lines)


def register_metadata(metadata):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we have to keep two systems to attach metadata to a class? the register_metadata vs the class attribute like _cp_plan?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is because for handling with stuff like _cp_plan we don't have a mixin like CPMixin. However for others, we have dedicated mixins.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But do we need this registration, though? All the available feature set can be queried through the main model class, no?



@dataclass
class TransformerBlockOutput(TransformerModuleOutput):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are not used yet no?

Comment thread src/diffusers/hooks/_helpers.py Outdated
_cached_parameter_indices: dict[str, int] = None

def _register(self, cls):
"""Attach this metadata to ``cls`` and register it in :class:`TransformerBlockRegistry`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single "" is what we use across diffusers. Also, :class:` isn't something we do.

Suggested change
"""Attach this metadata to ``cls`` and register it in :class:`TransformerBlockRegistry`.
"""Attach this metadata to `cls` and register it in `TransformerBlockRegistry`.

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a pass on LoRA. Will now do a pass on modeling_utils.py.

I think it might be better to do the LoRA-related separation in another PR because it's difficult to truly assess if it's in a working state.

  • We are likely missing andling Flux Control LoRA and as such, handling text-encoder LoRA modules. It's also not clear how this would affect lora_pipeline.py and lora_conversion_utils.py.
  • One-off utilities that are not shared across multiple different functions / classes.
  • It's not clear how the existing https://github.com/huggingface/diffusers/blob/main/src/diffusers/loaders/peft.py gets affected by this.

Comment thread src/diffusers/hooks/_helpers.py Outdated
_cls: Type = None
_cached_parameter_indices: dict[str, int] = None

def _register(self, cls):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little unclear about the docstring. How is this method metadata (which is what the docstring reads)?

Comment thread src/diffusers/hooks/_helpers.py Outdated
def _register(self, cls):
"""Attach this metadata to ``cls`` and register it in :class:`TransformerBlockRegistry`.

Lets ``@register_metadata(TransformerBlockMetadata(...))`` work for block classes that opt into the decorator

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Comment thread src/diffusers/hooks/_helpers.py Outdated
Comment on lines +42 to +44
self._cls = cls
cls._block_metadata = self
TransformerBlockRegistry._registry[cls] = self

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it only applicable to classes with _repeated_blocks set?



@maybe_allow_in_graph
@register_metadata(TransformerBlockMetadata(return_hidden_states_index=1, return_encoder_hidden_states_index=0))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

"_supports_cache": (
True,
"True",
"Supports caching techniques (PAB / FasterCache / FirstBlockCache) via `enable_cache`.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Supports caching techniques (PAB / FasterCache / FirstBlockCache) via `enable_cache`.",
"Supports caching techniques (e.g. FasterCache) via `enable_cache`.",

Comment thread src/diffusers/loaders/lora.py Outdated
r"""
Add an adapter to the underlying model.

``source`` can be either:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing called source here.

Comment thread src/diffusers/loaders/lora.py Outdated

_maybe_warn_for_unhandled_keys(incompatible_keys, adapter_name)

def _inject_adapter(self, state_dict, lora_config, adapter_name, peft_kwargs):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will prefer it inline as it's not shared.

Comment thread src/diffusers/loaders/lora.py Outdated
self._rollback_adapter(adapter_name, e)
raise

def _maybe_apply_deferred_hotswap_prep(self, lora_config):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Comment thread src/diffusers/loaders/lora.py Outdated
prepare_model_for_compiled_hotswap(self, config=lora_config, **self._lora_hotswap_kwargs)
self._lora_hotswap_kwargs = None

def _hotswap_adapter(self, state_dict, lora_config, adapter_name):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Comment thread src/diffusers/loaders/lora.py Outdated
self._rollback_adapter(adapter_name, e)
raise

def _rollback_adapter(self, adapter_name, error):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.



@dataclass
class AttnProcessorOutput(TransformerModuleOutput):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does AttnProcessorOutput has to live in src/diffusers/models/transformers/utils.py? Could it not be used by VAEs or other components under src/diffusers/models/?

class ModelMetadata:
"""Snapshot of a model class's feature attributes.

Constructed by :meth:`ModelMixin.metadata` — walks ``cls.__mro__`` collecting rows from each mixin's ``_metadata``

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:meth: can be dangerous abbreviation 🤪

Comment thread src/diffusers/models/modeling_utils.py Outdated
return "\n".join(lines)


def register_metadata(metadata):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is because for handling with stuff like _cp_plan we don't have a mixin like CPMixin. However for others, we have dedicated mixins.

Comment thread src/diffusers/models/modeling_utils.py Outdated
return "\n".join(lines)


def register_metadata(metadata):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But do we need this registration, though? All the available feature set can be queried through the main model class, no?

@DN6

DN6 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

@yiyixuxu @sayakpaul Realised that the scope of the PR is probably too wide and is leading to some difficulty with the reviews. I've updated the PR to reduce scope to only the essentials. I will open PRs for LoRA and TransformerRegistry changes in follow ups.

@yiyixu Yes, ideally we would want anything that's not Model Loading or Hub related to live in a dedicated Mixin. The current challenge with the refactor is not introducing backward breaking changes as we migrate models. e.g If we were to add a CPMixin for enable_parallelism and remove the method from ModelMixin (practically speaking this change should be okay, but it is technically backwards breaking). Which is why some methods still remain on ModelMixin.

For from_single_file, I would like to actually move this into core ModelMixin because apart from the auto config fetching and conversion, it should reuse all the same logic as from_pretrained. IMO it makes sense to have it live in ModelMixin. For now I've introduced a new WeightMappingMixin as a temporary measure because adding the new from_single_file method to ModelMixin would break single file loading for anything that's not Flux.

@yiyixuxu yiyixuxu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @DN6
I left some questions on the weight mapper

about this

ideally we would want anything that's not Model Loading or Hub related to live in a dedicated Mixin. ...

got it! the dedicated-mixin direction sounds good to me, but I think 1.0 is a good place to do this though (to deprecate the methods on ModelMixin and move them properly). How about we merge this PR and then do this togther in a follow-up? (@dg845 and I can help as well)



# Assigned to ``FluxTransformer2DModel`` as the ``_weight_mapping`` class attribute in ``flux/model.py``.
FLUX_WEIGHT_MAPPING = WeightMappingHandler(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

umm the WeightMappingHandler already has detect_confnig/map_to_diffusers etc, as methods, why can't we just subclass it here and override these diretly, instead of passing them in as callables? feel like it'd give clearer signature to document and we could even enforce them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this works for me. You mean something like this?

  @dataclass
  class FluxWeightMappingHandler(WeightMappingHandler):
      def detect_config(self, state_dict): ...
      def map_to_diffusers(self, state_dict, **kwargs): ...

  FLUX_WEIGHT_MAPPING = FluxWeightMappingHandler(available_configs=..., default_config="flux-dev", ...).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep!

Comment thread src/diffusers/models/attention.py Outdated
_default_processor_cls = None
_available_processors = []
_supports_qkv_fusion = True
_parallel_config = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up on this
any reason we need it here? it should live on the attention processor, no?

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@DN6
DN6 marked this pull request as ready for review July 1, 2026 18:54

@yiyixuxu yiyixuxu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, i left some comments
feel free to address as you see fit & merge


# ---- conversion ----

def map_to_diffusers(self, state_dict: dict, **kwargs) -> dict:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better if we raise a NotImplementedError here too to enforce this method

if source in key:
tensors = forward_fn(value, **ctx)
for target, tensor in zip(targets, tensors):
new_key = WeightMappingHandler.rename_key(key.replace(source, target), rename_patterns)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh maybe just make rename_key a regular function then if you need to use it this way

Default implementation returns ``None`` so the caller falls back to ``_default_config``. Subclasses override to
inspect the state_dict for architecture-distinguishing keys or shapes.
"""
return None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we throw a warning if there are multiple configs and this is not implemented?

DN6 added 2 commits October 29, 2026 19:32
# Conflicts:
#	src/diffusers/hooks/_helpers.py
#	src/diffusers/models/modeling_utils.py
#	src/diffusers/models/transformers/flux/model.py
#	src/diffusers/models/transformers/transformer_bria_fibo.py
# Conflicts:
#	src/diffusers/hooks/_helpers.py
#	src/diffusers/loaders/single_file_model.py

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants