Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix non-idempotent revert_weight_conversion corrupting trust_remote_code saves#46651

Closed
Bluear7878 wants to merge 3 commits into
huggingface:mainfrom
Bluear7878:fix-revert-weight-conversion-nonidempotent-rename
Closed

Fix non-idempotent revert_weight_conversion corrupting trust_remote_code saves#46651
Bluear7878 wants to merge 3 commits into
huggingface:mainfrom
Bluear7878:fix-revert-weight-conversion-nonidempotent-rename

Conversation

@Bluear7878

@Bluear7878 Bluear7878 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #46650

save_pretrained silently corrupts the checkpoint of a trust_remote_code model whose model_type has a
hardcoded conversion mapping in conversion_mapping.py (currently nomic_bert and jina_embeddings_v3).

A remote model is loaded by its own remote modeling code, which builds the state dict directly and never
runs the core conversion engine, so model._weight_conversions is None and its in-memory keys are already
in the original on-disk layout. On save, revert_weight_conversion (core_model_loading.py) fabricates
the mapping from the hardcoded table and applies its reverse anyway. For an unanchored WeightRenaming
whose target is a substring of its source — e.g. nomic_bert / jina_embeddings_v3
WeightRenaming("encoder.layers", "layers") — reverting keys that already carry the prefix double-prepends
it:

from transformers.core_model_loading import WeightRenaming

rev = WeightRenaming(r"encoder.layers", r"layers").reverse_transform()
print(rev.rename_source_key("encoder.layers.0.attn.out_proj.weight")[0])
# -> 'encoder.encoder.layers.0.attn.out_proj.weight'   (expected: unchanged)

End-to-end, AutoModel.from_pretrained("nomic-ai/nomic-embed-text-v1", trust_remote_code=True)
save_pretrained(d)from_pretrained(d, trust_remote_code=True) reports every encoder.layers.* weight
as missing and silently keeps the randomly-initialised weights — accuracy collapses, with no error raised.

Fix

The fabricated mapping only ever applies to a model whose in-memory keys are in the canonical (native)
layout. That is never the case for a trust_remote_code model — its keys are already on-disk. So skip the
fabricated revert when the model class lives in the dynamic transformers_modules namespace, and revert
native models (built via from_config, whose keys are canonical) exactly as before.

Alternatives considered and ruled out:

  • Detecting "keys already on disk" per-key (does the forward mapping still rename a key?): false-positives
    on native models whose canonical keys still match an unanchored forward rename — it wrongly skipped their
    legitimate revert and broke test_reverse_loading_mapping for ~10 models. The canonical-vs-disk question
    is undecidable per-key for the X.model -> X mapping family.
  • Anchoring each affected mapping (^encoder\.layers): only patches one model and breaks the nomic_bert
    wrapper classes (base_model_prefix nesting under nomic_bert.) on both load and save.
  • Migrating these mappings to PrefixChange (à la Move some conversion mappings to PrefixChange #45567): does not address the unanchored reverse rename
    and still requires per-model migration.

Tests

tests/utils/test_core_model_loading.py::TestRevertWeightConversionRemoteCode:

  • a trust_remote_code model (class in transformers_modules) with on-disk-layout keys is saved unchanged
    (no encoder.encoder.*),
  • a native model (built via from_config) with canonical keys still round-trips to the on-disk layout.

The full test_reverse_loading_mapping suite (which builds native models) is unchanged by this fix. ruff check + ruff format clean.

Code Agent Policy

  • I confirm that this is not a pure code agent PR.

Before submitting

Who can review?

@Cyrilvallez — this directly extends your #45567 (PrefixChange filter in revert_weight_conversion).

…ode saves

When a model is saved without having been loaded through the conversion engine
(`model._weight_conversions is None`) -- e.g. a `trust_remote_code` model whose
remote `from_pretrained` builds the state dict directly, or a `from_config` model
-- `revert_weight_conversion` fabricates the mapping from the hardcoded table and
applies its reverse. For a mapping whose reverse rename is not idempotent (an
unanchored rename whose target is a substring of its source, e.g. nomic_bert and
jina_embeddings_v3 `WeightRenaming("encoder.layers", "layers")`), reverting keys
that are already in on-disk layout double-prepends the prefix
(`encoder.layers` -> `encoder.encoder.layers`), silently dropping every affected
weight on reload.

Only revert the fabricated mapping when the in-memory keys are actually in the
canonical (post-load) layout: if applying the forward renamings would still rename
a key, the keys are already in disk layout and must not be reverted. This preserves
the documented `from_config` -> original-format round-trip while fixing the
corruption for the whole substring-doubling family (nomic_bert, jina_embeddings_v3,
llava and aliases, emu3, colqwen2, detr).
… heuristic

The previous "would the forward mapping rename any key?" check false-positived on
native `from_config` models whose canonical keys still match an unanchored forward
rename (e.g. dinov3_vit `(?<!model.)layer.`, qianfan_ocr `encoder.layers.`), wrongly
skipping their legitimate revert and breaking `test_reverse_loading_mapping` for
~10 models.

Disk-layout in-memory keys only occur for `trust_remote_code` models (the remote
modeling code builds the state dict directly and never runs the conversion engine);
native models always carry canonical keys. Skip the fabricated revert only when the
model class lives in the dynamic `transformers_modules` namespace, leaving native
models untouched.
…ration idempotent

Match `transformers_modules.` with a trailing dot so an unrelated top-level module that merely
starts with `transformers_modules` cannot match, and pass `overwrite=True` when registering the
test conversion mappings so the tests can be re-run in the same process.
@github-actions

Copy link
Copy Markdown
Contributor

CI Dashboard: View test results in Grafana

@Bluear7878

Copy link
Copy Markdown
Contributor Author

@Cyrilvallez this fixes a silent save_pretrained corruption for trust_remote_code models (nomic_bert / jina_embeddings_v3) — it directly extends your #45567. Would you be able to take a look when you get a chance? CircleCI is green; the pr-ci reds are a self-hosted-runner infra failure (0 actual test failures).

@Cyrilvallez

Copy link
Copy Markdown
Member

Thanks for the PR! The proper fix is in #46876 though! Let me know if something is unclear

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

save_pretrained corrupts keys (encoder.encoder.*) for trust_remote_code / from_config models via non-idempotent reverse rename

2 participants