Skip to content

🚨 Generalize get_decoder() for multimodal and delete redundant code 🔪 #42156

Merged
zucchini-nlp merged 12 commits intohuggingface:mainfrom
zucchini-nlp:get-submodels
Nov 19, 2025
Merged

🚨 Generalize get_decoder() for multimodal and delete redundant code 🔪 #42156
zucchini-nlp merged 12 commits intohuggingface:mainfrom
zucchini-nlp:get-submodels

Conversation

@zucchini-nlp
Copy link
Member

@zucchini-nlp zucchini-nlp commented Nov 12, 2025

What does this PR do?

As per title, blocked by #41589 for VLMs! We should be able to use get_decoder() to get the LM part of any model after this and have much less duplicate code. Same foes for the get_encoder() to get the encoder if the model has a separate encoding module. In comparison to decoder, we can have specific encoder per modality so the helper will accept modality as arg

Universal helper first reduces duplicate code, nudges us to use standardized names for major modules and can be used by 3rd party libraries. Right now we have 5 ways to name a vision encoder!

🚨 Breaking changes, ig we can break helpers for v5:

  • VLMs now will not have a property to get self.language_model directly from task-model and users will need to call self.get_decoder()
  • Deleted get_text_encoder and get_audio_encoder in some audio models because functionality is covered now by the get_encoder()

@HuggingFaceDocBuilderDev

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.

@zucchini-nlp zucchini-nlp changed the title [WIP] Generalize get_decoder() for multimodal and delete redundant code 🔪 🚨 Generalize get_decoder() for multimodal and delete redundant code 🔪 Nov 13, 2025
model = GPT2LMHeadModel(cfg)
dec = model.get_decoder()

assert dec is model, f"GPT2 get_decoder() should return self (fallback), got {type(dec)}"
Copy link
Member Author

@zucchini-nlp zucchini-nlp Nov 13, 2025

Choose a reason for hiding this comment

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

prev helper didn't cover all edge cases! This should be the base model, if we compare with other LLMs (e.g. llama)

Copy link
Contributor

@molbap molbap left a comment

Choose a reason for hiding this comment

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

Very nice unbloating 🔪
OK for me, just would be cool to add to the make style/ruff rules/quality check to reduce cognitive load

Symmetric setter. Mirrors the lookup logic used in `get_encoder`.
"""

# NOTE: new models need to use existing names for layers if possible, so this list doesn't grow infinitely
Copy link
Contributor

Choose a reason for hiding this comment

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

To note, this should be enforced in make fixup in code consistency part to save ourselves the hassle

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm, isn't it going to be a huge limitation for contributors if we force it and auto-renam with fix-copies? Imo we need to communicate it when reviewing and explain why it's important. It's only a few ppl reviewing VLMs currently, so it might be easier

Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking the make fixup updated message (or rather code-quality check on the CI, same) would be informative enough, saying "decoder layer names should be part of this list: ..." rather than auto-renaming. Could be a ruff warning if we think it's too restrictive as an error?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, lemme see where I can fit this in a non-disruptive way. Not sure if users actually read the warnings, we should be more strict in review process in any case imo 😆

@zucchini-nlp
Copy link
Member Author

Merge conflicts after a big refactor 😢

@github-actions
Copy link
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: aria, autoformer, aya_vision, bart, bigbird_pegasus, blenderbot, blenderbot_small, blip_2, cohere2_vision, colqwen2, conditional_detr, d_fine, dab_detr, deformable_detr, detr, dia

@zucchini-nlp
Copy link
Member Author

hey @jackzhxng, I remember you requested this feature for torch.export. Now you can

multimodal_model.get_decoder() -> returns the decoding LM
multimodal_model.get_encoder() -> returns the encoding LM if any
multimodal_model.get_encoder(modality="image") -> returns the encoding vision tower if any
multimodal_model.get_encoder(modality="audio") -> returns the encoding audio tower if any

also cc @hmellor, we also discussed it re vLLM

@zucchini-nlp zucchini-nlp merged commit e2fb8d6 into huggingface:main Nov 19, 2025
23 checks passed
@BenjaminBossan
Copy link
Member

Hi @zucchini-nlp this PR causes an issue with PEFT as (at least some) decoder models now have get_encoder. As an example:

from transformers import AutoModelForCausalLM

model_id = "facebook/opt-125m"
model = AutoModelForCausalLM.from_pretrained(model_id)
assert not hasattr(model, "get_encoder")
# after this PR, model.get_encoder() returns model.model

This works with the previous commit (a5c903f877fda21e739027eed133e03162eb7712) but fails after this PR (e2fb8d6062a05f69f976cf6e39618df6c31a3bfd). Is this change intended?

@jackzhxng
Copy link
Contributor

@zucchini-nlp this is amazing thank you!

@zucchini-nlp
Copy link
Member Author

@BenjaminBossan ideally it should return self and not the base model. I see where it comes from and will fix it. Then you can check with smth like - self.get_encoder != self: encoder = self.get_encoder()

All models will have get_encoder() implemented, but if a model has no actual encoder it should return self

@BenjaminBossan
Copy link
Member

I see where it comes from and will fix it.

Great, please let me know when the PR is there.

All models will have get_encoder() implemented, but if a model has no actual encoder it should return self

We can modify PEFT to take this into account. But at least to me, this API feels a bit strange to be honest.

@zucchini-nlp
Copy link
Member Author

@BenjaminBossan yeah, it is because we have get_encoder() in PreTrainedModel so all models will have it as an attribute. Might feel weird though it is same as it was with get_decoder() and I think it saves us from duplicating the same code in all models

BenjaminBossan added a commit to BenjaminBossan/peft that referenced this pull request Nov 20, 2025
When using mixed adapter batches (i.e. using different LoRA adapters in
the same batch), users have to pass adapter_names. When simultaneously
using beam search, these adapter names have to be extended by the number
of beams. For encoder-decoder models, even when applying beam search,
the encoder part of the model should, however, not use the extended
adapter_names. This is because the encoder still uses the original,
non-extended samples.

The need for this used to be checked by calling model.get_encoder().
However, with transformers v5, every PretrainedModel will have a
get_encoder method. The new convention is that it will return self if
there is no encoder. This is now what's being checked.

huggingface/transformers#42156

Note that said PR contains a small bug that leads to self not always
being returned. Therefore, for the full fix of the issue on transformers
main, we also need to await this PR:

huggingface/transformers#42295
BenjaminBossan added a commit to huggingface/peft that referenced this pull request Nov 20, 2025
When using mixed adapter batches (i.e. using different LoRA adapters in
the same batch), users have to pass adapter_names. When simultaneously
using beam search, these adapter names have to be extended by the number
of beams. For encoder-decoder models, even when applying beam search,
the encoder part of the model should, however, not use the extended
adapter_names. This is because the encoder still uses the original,
non-extended samples.

The need for this used to be checked by calling model.get_encoder().
However, with transformers v5, every PretrainedModel will have a
get_encoder method. The new convention is that it will return self if
there is no encoder. This is now what's being checked.

huggingface/transformers#42156

Note that said PR contains a small bug that leads to self not always
being returned. Therefore, for the full fix of the issue on transformers
main, we also need to await this PR:

huggingface/transformers#42295
Conzel pushed a commit to Conzel/peft that referenced this pull request Nov 25, 2025
When using mixed adapter batches (i.e. using different LoRA adapters in
the same batch), users have to pass adapter_names. When simultaneously
using beam search, these adapter names have to be extended by the number
of beams. For encoder-decoder models, even when applying beam search,
the encoder part of the model should, however, not use the extended
adapter_names. This is because the encoder still uses the original,
non-extended samples.

The need for this used to be checked by calling model.get_encoder().
However, with transformers v5, every PretrainedModel will have a
get_encoder method. The new convention is that it will return self if
there is no encoder. This is now what's being checked.

huggingface/transformers#42156

Note that said PR contains a small bug that leads to self not always
being returned. Therefore, for the full fix of the issue on transformers
main, we also need to await this PR:

huggingface/transformers#42295
@jackzhxng
Copy link
Contributor

@zucchini-nlp I believe Voxtral actually still has top-level language_model - https://github.com/huggingface/transformers/blob/main/src/transformers/models/voxtral/modeling_voxtral.py#L379

Additionally woult it be possible for get_decoder to return the causal variant of the model which includes the lm_head?

@zucchini-nlp
Copy link
Member Author

Ah, Voxtral has a causal model as backbone. We need to explicitly overwrite get_decoder for the model in that case, will do

lancerts added a commit to linkedin/Liger-Kernel that referenced this pull request Dec 15, 2025
## Summary

This PR fixes access to missing attributes for multimodal models in
`src/liger_kernel/transformers/monkey_patch.py`. The main change is to
consistently access attributes (like `language_model`, `vision_tower`,
and `visual`) through the submodel `.model` attribute of the parent
model, rather than directly from the parent model itself.

This fixes AttributeError after this PR was merged in transformers:
- huggingface/transformers#42156

See associated issue in TRL:
- huggingface/trl#4601

Fix #960.

## Details

Fix: Consistent attribute access via `.model`

* Updated all references to submodules such as `language_model`,
`vision_tower`, and `visual` to use the `.model` attribute (e.g.,
`model.model.language_model` instead of `model.language_model`) across
all kernel application functions for models including LLava, Mllama,
Gemma3, PaliGemma, Qwen2 VL, Qwen2.5 VL, Qwen3 VL, Qwen3 VL MoE, GLM4V,
GLM4V MoE, and InternVL.

Normalization and patching logic updates

* Adjusted normalization and patching calls to operate on submodels
accessed via `.model`, ensuring that layer normalization and RMS
normalization are consistently applied to the correct components.

These changes make the codebase more maintainable and robust against
future changes in model class implementations.

## Testing Done

- Hardware Type: <BLANK>
- [ ] run `make test` to ensure correctness
- [ ] run `make checkstyle` to ensure code style
- [ ] run `make test-convergence` to ensure convergence

---------

Co-authored-by: Shao Tang <tangshao28@gmail.com>
@zucchini-nlp
Copy link
Member Author

Oh, which includes the lm_head, I misread it!

Nah, the base model is supposed to be the model without any task head on top so in Voxtral, it is going to be the language model without a head

Arlaz pushed a commit to obvious-research/peft that referenced this pull request Jan 9, 2026
When using mixed adapter batches (i.e. using different LoRA adapters in
the same batch), users have to pass adapter_names. When simultaneously
using beam search, these adapter names have to be extended by the number
of beams. For encoder-decoder models, even when applying beam search,
the encoder part of the model should, however, not use the extended
adapter_names. This is because the encoder still uses the original,
non-extended samples.

The need for this used to be checked by calling model.get_encoder().
However, with transformers v5, every PretrainedModel will have a
get_encoder method. The new convention is that it will return self if
there is no encoder. This is now what's being checked.

huggingface/transformers#42156

Note that said PR contains a small bug that leads to self not always
being returned. Therefore, for the full fix of the issue on transformers
main, we also need to await this PR:

huggingface/transformers#42295
SangbumChoi pushed a commit to SangbumChoi/transformers that referenced this pull request Jan 23, 2026
… 🔪 (huggingface#42156)

* update some models

* update the rest

* add helper for encoder

* delete encoder code from models

* fix copies

* fix some tests but VLM will fail

* add encider tests simialr to decoder

* no print

* fix overwritten models

* and a million exceptions with old audio models, revert back
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

Comments