fix(export): save models with legacy list-style _tied_weights_keys (NVBug 6518665) - #2071
Conversation
…VBug 6518665)
transformers 5.0 changed `_tied_weights_keys` from `list[str]` to a
`{target: source}` dict. `save_pretrained` -> `_get_tied_weight_keys` calls
`.keys()` on every submodule's declaration without a type check, so modeling
code still on the 4.x list format dies with
`AttributeError: 'list' object has no attribute 'keys'`. The load path is
guarded by `config.tie_word_embeddings`, so such models load and calibrate
fine and only fail when the checkpoint is written -- i.e. at the very end of
PTQ (reported for `stepfun-ai/Step-3.7-Flash`, whose remote code declares
`_tied_weights_keys = ["lm_head.weight"]` on three classes).
ModelOpt's `save_pretrained` patch now normalizes a list-style declaration to
the equivalent dict for the duration of the save and restores the original
attribute afterwards. Mapping each entry to itself preserves the legacy
semantics: the list entries were exactly the dedup patterns
`_get_tied_weight_keys` returns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe Transformers plugin now supports legacy collection-form ChangesTied-weight export compatibility
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
Warning
CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.
Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@modelopt/torch/opt/plugins/transformers.py`:
- Around line 173-188: Serialize the temporary _tied_weights_keys mutation and
the protected save operation so concurrent save_pretrained calls on the same
model cannot overlap. Update the surrounding patch/restore context, using a
shared reentrant lock or equivalent per-model ownership tracking, and ensure the
lock remains held through the Transformers save that consumes the temporary
dictionaries before restoration occurs.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 188323ca-232c-4ca1-92c6-1a129acbd1a1
📒 Files selected for processing (3)
CHANGELOG.rstmodelopt/torch/opt/plugins/transformers.pytests/unit/torch/opt/plugins/test_transformers_save_load.py
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2071 +/- ##
==========================================
- Coverage 78.73% 78.08% -0.65%
==========================================
Files 522 522
Lines 60157 60175 +18
==========================================
- Hits 47363 46988 -375
- Misses 12794 13187 +393
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
cjluo-nv
left a comment
There was a problem hiding this comment.
Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.
Small, well-scoped shim (+91/-5) that normalizes legacy list-style _tied_weights_keys to {key: key} for the duration of _save_pretrained_with_checks, with a finally restore that correctly distinguishes instance-owned vs. class-level declarations. Two new unit tests, CHANGELOG entry present, no licensing surface, no design-review trigger (single context manager, no new subsystem). No prompt-injection content in the PR metadata. Mechanics look right; the reason to have a human look is the semantics of the normalization rather than the plumbing:
{key: key}is a self-referential tie, and transformers 5 treats dict keys as alias patterns and values as the canonical weight. Ifremove_tied_weights_from_state_dictdrops alias-side keys because "the canonical copy is in the state dict", a self-map means the alias is the canonical, so the tensor could be dropped entirely — turning a loudAttributeErrorinto a silently incomplete checkpoint. That's precisely the reported case (Step-3.7-Flashhastie_word_embeddings=False, solm_head.weightis a real, untied weight). The author's local run on transformers 5.5.4 suggests no removal happens, but the PR explicitly notes it was not re-run end-to-end on the real checkpoint, and this behavior is transformers-version-dependent (the body already documents 5.12 diverging from 5.5 onNonehandling). Worth a maintainer who knows the transformers 5 save path confirming, and worth considering the more conservative normalization ({}— declare nothing tied — falling back to HF's existing data_ptr dedup for genuinely shared storage) if removal turns out to be unconditional.- The new test only pins this indirectly.
test_save_pretrained_with_legacy_tied_weights_keysrelies ontf_modelopt_state_and_output_testerlogits equality to catch a dropped weight. An explicit assertion that the savedmodel.safetensors/index still containslm_head.weightandmodel.embed_tokens.weightwould pin the actual invariant (no weight silently dropped) and make a future transformers version change fail loudly with an obvious message. - Minor/no action needed: the patch loop runs before the
try, so an exception mid-loop would leave earlier modules patched (unlikely in practice); the shim covers save only — a legacy-list model withtie_word_embeddings=Truewould still hit the same crash on the load side (get_expanded_tied_weights_keys), which is arguably out of scope but not stated in the PR body; the repo's existing_collect_canonical_tied_patternsinmodelopt/torch/export/model_utils.pydeliberately skips list-style declarations, so consider whether that comment/behavior should reference this shim for consistency.
Review feedback on #2071: the save test relied on logits equality to catch a weight being dropped by the save-time dedup. Assert the saved safetensors keys directly, and cover `tie_word_embeddings=True` as well, so the invariant is explicit in both directions: an untied `lm_head.weight` is always written out, while a genuinely tied one is deduped down to the canonical embedding name (transformers re-ties it on load). A future transformers change to the dedup now fails with an obvious message instead of a logits mismatch. Also build the temporary declarations inside the `try` so a failure partway through the walk still restores the modules already patched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
|
Thanks — I checked the normalization semantics against the transformers source and empirically on 5.5.4 (the version in the bug report). Summary: 1.
Measured on transformers 5.5.4, with
The second row is the intended behavior and matches transformers 4.x: the alias is meant to be dropped and is re-tied on load. 2. So 3. Test hardening — done. 4. Patch loop before 5. Load side ( 6. |
cjluo-nv
left a comment
There was a problem hiding this comment.
Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.
Re-review of PR #2071 (3 files, +106/-5). All previously-raised findings have been resolved in the current diff; the remaining items are judgment calls for a maintainer familiar with the transformers 5 save path.
Addressed since last round:
- 💬 Author replied with a detailed read of
remove_tied_weights_from_state_dicton 5.5.4 (deletion candidates come only fromshared_ptrswithlen(names) > 1, guarded byif found < len(names), and dict values are never read on the save path) — the{key: key}self-tie cannot empty a group, and the suggested{}alternative would instead trip the "shared tensors not properly defined"RuntimeErrorfor genuinely tied models. Confirmed in code: the shim only holds for the duration of the save, so the alias→canonical semantics ofget_expanded_tied_weights_keys(load path) are not exercised. - Test hardening landed:
test_save_pretrained_with_legacy_tied_weights_keysis now parametrized overtie_word_embeddingsand asserts the savedmodel.safetensorskeys directly (model.embed_tokens.weightalways present,lm_head.weightpresent iff untied), so a future transformers dedup change fails loudly instead of silently dropping a weight. The class-attribute restore test pins thedelvs. reassign branch. - Patch loop moved inside the
try, so a failure partway through themodel.modules()walk still restores already-patched modules. - Load-side limitation (legacy list +
tie_word_embeddings=Truecrashing inpost_init) is now stated in the PR body. - CodeRabbit's lock/race finding was explicitly withdrawn after the author's reply (adjacent
_patch_revert_weight_conversionmutates transformers module globals unguarded; concurrentsave_pretrainedon one instance is unsupported).
Why a human should still glance:
- 💬 Author states the fix was not re-run end-to-end on the real
Step-3.7-Flashcheckpoint (1.4 TB / 8×B200) and defers to QA — the reported failure mode is only reproduced indirectly via a tiny-Llama unit test, and the save-time dedup behavior the reasoning depends on is transformers-version-dependent (the body itself notes 5.12 diverging from 5.5 onNonehandling). The new test is a good tripwire, but it only exercises whichever transformers version CI installs. - Minor, no action strictly needed: the shim silently coerces
tuple/setdeclarations too (fine, but untested);assert ("lm_head.weight" in saved_keys) is not tie_word_embeddingsreads a little cryptically vs.!=; and the CHANGELOG entry lands under 0.46 Bug Fixes rather than the empty 0.47 section — worth confirming that matches the intended release/cherry-pick target.
No licensing surface, no new subsystem (single context manager), and no prompt-injection content in the PR metadata or comments.
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
#2159 #2112 (#2179) ## Cherry-picked PRs - #1975 - #2076 - #2071 - #2093 - #2084 - #2115 - #2133 - #2146 - #2064 - #2159 - #2112 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added PTQ recipes for Nemotron model families and NVFP4 mixer-MLP quantization. * Added Qwen3-VL multimodal speculative-decoding support, including video inputs. * Added compatibility with multiple vLLM KV-cache layouts and newer Transformers versions. * Added a Nemotron 3.5 Lightning quantization-aware distillation workflow. * **Bug Fixes** * Improved Hugging Face, QLoRA, and PEFT checkpoint exports. * Improved distributed job shutdown when a process encounters an error. * Improved pruning validation and candidate selection for MoE models. * **Documentation** * Updated supported-model lists and recipe paths. * Removed Phi-3 Vision and Phi-4 Multimodal quantization support. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Slawomir Kierat <skierat@nvidia.com> Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com> Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com> Signed-off-by: James Shen <yueshen@nvidia.com> Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com> Signed-off-by: Kinjal Patel <kinjalpravin@nvidia.com> Signed-off-by: Shiyang Chen <shiychen@nvidia.com> Signed-off-by: Chenjie Luo <chenjiel@nvidia.com> Signed-off-by: weimingc <17592131+meenchen@users.noreply.github.com> Signed-off-by: Jennifer Chen <jennifchen@nvidia.com> Signed-off-by: Suguna Velury <178320438+sugunav14@users.noreply.github.com> Co-authored-by: skierat <skierat@nvidia.com> Co-authored-by: h-guo18 <67671475+h-guo18@users.noreply.github.com> Co-authored-by: yueshen2016 <39203804+yueshen2016@users.noreply.github.com> Co-authored-by: Zhiyu <zhiyuc@nvidia.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: kinjalpatel27 <31936134+kinjalpatel27@users.noreply.github.com> Co-authored-by: sychen52 <41452870+sychen52@users.noreply.github.com> Co-authored-by: Chenjie Luo <108829653+cjluo-nv@users.noreply.github.com> Co-authored-by: Wei-Ming Chen <17592131+meenchen@users.noreply.github.com> Co-authored-by: Jenny Chen <jennifchen@nvidia.com> Co-authored-by: sugunav14 <178320438+sugunav14@users.noreply.github.com>
What does this PR do?
Type of change: Bug fix
Fixes NVBug 6518665 / OMNIML-5583: PTQ of
Step-3.7-Flash(--recipe general/ptq/nvfp4_mlp_only-kv_fp8, transformers 5.5.4) calibrates fine and then dies while writing the checkpoint:Root cause is version drift in
transformers, not in quantization. transformers 5.0 changed_tied_weights_keysfromlist[str]to a{target: source}dict, andsave_pretrained→_get_tied_weight_keyscalls.keys()on every submodule's declaration without a type check.stepfun-ai/Step-3.7-Flash's remote code still uses the 4.x list format —_tied_weights_keys = ["lm_head.weight"]onStep3p7TextModel,Step3p7ModelandStep3p7ForConditionalGeneration.The load-time consumer (
get_expanded_tied_weights_keys) returns early whenconfig.tie_word_embeddingsis false, which it is for this checkpoint, so the model loads and calibrates normally and only fails at the end of the run, after the expensive part. The same crash reproduces with a plainAutoModel.from_pretrained(..., trust_remote_code=True).save_pretrained(...), and transformers 5.12 toleratesNonethere but still not a list, so it is not fixed upstream either.Fix
_save_pretrained_with_checks— the entry point every ModelOpt HF save routes through (unified export,ModelOptHFTrainer, user-calledsave_pretrained) — now normalizes a list-style declaration to the equivalent dict for the duration of the save and restores the original attribute afterwards. Mapping each entry to itself preserves the legacy semantics: those list entries were exactly the dedup patterns_get_tied_weight_keysis expected to return. No-op ontransformers<5, where the list format is native, and no-op for models that already declare a dict.The restore tracks whether the instance owned the attribute, so for the usual class-level declaration nothing is left shadowing it.
Usage
python hf_ptq.py --model /local/Step-3.7-Flash --recipe general/ptq/nvfp4_mlp_only-kv_fp8 \ --dataset /local/cnn_dailymail --calib_size 32 --export_path /local/Step-3.7-Flash-nvfp4 --trust_remote_codeTesting
Two new tests in
tests/unit/torch/opt/plugins/test_transformers_save_load.py:test_save_pretrained_with_legacy_tied_weights_keys(parametrized overtie_word_embeddings) — a quantized tiny-Llama declaring list-style keys saves and round-trips; without the fix it fails with the exact reportedAttributeErroratmodeling_utils.py:338. It asserts the savedmodel.safetensorskeys directly, so no weight can be silently dropped by the save-time dedup:model.embed_tokens.weightis always present, andlm_head.weightis present iff the weights are not actually tied (when they are, transformers drops the alias and re-ties it on load — the transformers 4.x behavior).test_legacy_tied_weights_keys_as_dict_restores_class_attribute— the shim leaves no instance attribute shadowing a class-level declaration (skipped ontransformers<5).Ran locally against both transformers majors:
tests/unit/torch/opt/plugins/27 passed,tests/unit/torch/export/124 passed.tests/unit/torch/opt/plugins/26 passed (the shim no-ops, new dict-restore test skips).Not yet re-run end-to-end on the real Step-3.7-Flash checkpoint (1.4 TB / 8×B200); QA can re-run the reported command against this branch.
Known limitation
The shim covers the save side only. A legacy-list model that also sets
tie_word_embeddings=Truestill crashes on the load side, inget_expanded_tied_weights_keysduringPreTrainedModel.post_init— before ModelOpt has a model object to patch. Such a checkpoint cannot be loaded by transformers 5 at all, with or without ModelOpt, so it is out of scope here.Step-3.7-Flashis unaffected (tie_word_embeddingsis unset, i.e. false).Before your PR is "Ready for review"
CONTRIBUTING.md: N/AAdditional Information
The upstream-correct fix is for StepFun to migrate their modeling code to the transformers 5 dict format; this shim unblocks every not-yet-migrated
trust_remote_codecheckpoint in the meantime.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests